SpringBoot整合SpringSecurity(五)权限控制

Spring Security具有强大的权限验证。

权限有些人认为是页面的隐藏,其实不然。权限可以理解为是否可以访问资源,页面隐藏什么的是客户友好度的事情,所以对于web而言,系统的安全不安全,最终取决于对url的控制。

本文章代码可以参考 https://gitee.com/Maoxs/security-test中的 security-permission

准备

页面

首先呢是登陆

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h3>表单登录</h3>
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2">
<button type="button" onclick="login()">登录</button>
</td>
</tr>
</table>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script>
function login() {
var username = $("input[name=username]").val();
var password = $("input[name=password]").val();
if (username === "" || password === "") {
alert("用户名或密码不能为空");
return;
}
$.ajax({
type: "POST",
url: "/authentication/form",
data: {
"username": username,
"password": password
},
success: function (e) {
console.log(e);
alert("登陆成功")
setTimeout(function () {
location.href = '/hello';
}, 1500);
},
error: function (e,a,b) {
console.log(e.responseText);
alert("登陆失败")
}
});
}

</script>
</body>
</html>

然后是我自己测试权限的页面

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登陆成功</h1>
<button onclick="check(this)" data-href="/java">检测java角色</button>
<button onclick="check(this)" data-href="/docker">检测docker角色</button>
<button onclick="check(this)" data-href="/php">检测php角色</button>
<button onclick="check(this)" data-href="/custom">检测自定义匹配器</button>
<a href="/logout">退出登录</a>
</body>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script>
function check(e) {
var url = e.dataset.href;
$.ajax({
type: "POST",
url: url,
success: function (e) {
alert(e)
},
error: function (e, a, b) {
console.log(e);
alert("没有权限")
}
});
}
</script>
</html>

数据

这里呢我们就不去具体的访问数据库了,准备一些模拟的数据,一个用户可以用多个角色和权限。

首先俩实体

@Data
public class SysRole implements Serializable {
private Long id;
private String roleName;
public SysRole(Long id,String roleName){
this.id=id;
this.roleName=roleName;
}
}
@Data
public class SysUser implements Serializable {

private Long id;
private String userName;
private String password;
private List<SysRole> roles;


public SysUser(Long id, String userName, String password, List<SysRole> roles) {
this.id = id;
this.userName = userName;
this.password = password;
this.roles = roles;
}

}
public class InitData {

public static final Set<SysUser> SYS_USERS = new HashSet<>();

public static final Set<SysRole> SYS_ROLES = new HashSet<>();

static {
SYS_ROLES.add(new SysRole(1L, "ROLE_JAVA");
SYS_ROLES.add(new SysRole(2L, "ROLE_DOCKER");
SYS_ROLES.add(new SysRole(3L, "ROLE_PHP");
SYS_ROLES.add(new SysRole(4L, "ROLE_PYTHON");
SYS_ROLES.add(new SysRole(5L, "ROLE_CENTOS");
}

static {
SYS_USERS.add(
new SysUser(1L, "fulin", "123456",
SYS_ROLES.stream().filter(o -> StringUtils.equalsAny(o.getRoleName(), "ROLE_JAVA", "ROLE_DOCKER")).collect(Collectors.toList())
)
);
SYS_USERS.add(
new SysUser(2L, "maoxiansheng", "123456",
SYS_ROLES.stream().filter(o -> StringUtils.equalsAny(o.getRoleName(), "ROLE_PHP", "ROLE_DOCKER")).collect(Collectors.toList())
)
);
SYS_USERS.add(
new SysUser(3L, "happy fish", "123456",
SYS_ROLES.stream().filter(o -> StringUtils.equalsAny(o.getRoleName(), "ROLE_PYTHON", "ROLE_CENTOS")).collect(Collectors.toList())
)
);
}

}

然后是UserDetailsService

@Service
public class UserService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
SysUser sysUser = InitData.SYS_USERS.stream().filter(o -> StringUtils.equals(o.getUserName(), s)).findFirst().orElse(null);
if (sysUser == null) {
throw new UsernameNotFoundException("用户不存在");
}
//模拟从数据库获取角色权限
Collection<GrantedAuthority> authorities = new ArrayList<>();
List<SysRole> roles = sysUser.getRoles();
for (SysRole role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
return new User(sysUser.getUserName(), sysUser.getPassword(), authorities);
}

}

controller

@Controller
@RequestMapping
public class TestController {
@RequestMapping("/hello")
public String hello() {
return "hello";
}
@RequestMapping("/login")
public String login() {
return "login";
}
}

然后是需要鉴权的controller

@RestController
public class PermissionController {
@RequestMapping("/docker")
public String test1() {
return "说明你有docker权限";
}
@RequestMapping("/custom")
public String test0() {
return "说明你有自定义权限";
}
@RequestMapping("/java")
public String test2() {
return "说明你有java权限";
}
@RequestMapping("/php")
public String test3() {
return "说明你有最好语言的权限";
}
}

然后就进入重头戏了

权限配置

这一篇内我们主要讲在configure 中配置的形式,下一篇会说明比较强大的 权限注解。

先看配置

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

private final FailureAuthenticationHandler failureAuthenticationHandler;
private final SuccessAuthenticationHandler successAuthenticationHandler;
private final UserService userService;
private final AccessDeniedAuthenticationHandler accessDeniedAuthenticationHandler;


public WebSecurityConfig(UserService userService, FailureAuthenticationHandler failureAuthenticationHandler, SuccessAuthenticationHandler successAuthenticationHandler,AccessDeniedAuthenticationHandler accessDeniedAuthenticationHandler) {
this.userService = userService;
this.failureAuthenticationHandler = failureAuthenticationHandler;
this.successAuthenticationHandler = successAuthenticationHandler;
this.accessDeniedAuthenticationHandler = accessDeniedAuthenticationHandler;
}
/**
* 注入身份管理器bean
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 注入自定义权限管理
*
* @return
* @throws Exception
*/
@Bean
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setPermissionEvaluator(new CustomPermissionEvaluator());
return handler;
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(
new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}

@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
});
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.failureHandler(failureAuthenticationHandler) // 自定义登录失败处理
.successHandler(successAuthenticationHandler) // 自定义登录成功处理
.and()
.logout()
.logoutUrl("/logout")
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/authentication/form") // 自定义登录路径
.and()
.authorizeRequests()// 对请求授权
.antMatchers("/login", "/authentication/require",
"/authentication/form").permitAll()// 这些页面不需要身份认证
.antMatchers("/docker").hasRole("DOCKER")
.antMatchers("/java").hasRole("JAVA")
.antMatchers("/java").hasRole("JAVA")
.antMatchers("/custom")
.access("@testPermissionEvaluator.check(authentication)")
.anyRequest()//其他请求需要认证
.authenticated().and().exceptionHandling()
.accessDeniedHandler(accessDeniedAuthenticationHandler)
.and()
.csrf().disable();// 禁用跨站攻击
}

}

这里呢我们要说明一下

authorizeRequests() 就是请求授权,然后其中的antMatchers() 就是匹配对应的url。

我们看到后面的permitAll()hasRole("ROLE_DOCKER") 这些都可以叫做权限表达式。我总结了一些差不多有这么多

表达式 说明
hasRole([role]) 用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀)
hasAnyRole([role1,role2]) 用户拥有任意一个制定的角色时返回true
hasAuthority([authority]) 等同于hasRole,但不会带有ROLE_前缀
asAnyAuthority([auth1,auth2]) 等同于hasAnyRole
permitAll 永远返回true
denyAll 永远返回false
authentication 当前登录用户的authentication对象
fullAuthenticated 当前用户既不是anonymous也不是rememberMe用户时返回true
hasIpAddress('192.168.1.0/24')) 请求发送的IP匹配时返回true

自定义权限表达式

然后来就要说一下这个access() 了,spring3.0后出了spel 超好用,有了这个我们就可以设置自己的权限验证了

比如说可以组合操作access("hasRole('JAVA') or hasRole('DOCKER')")

我写的这句 access("@testPermissionEvaluator.check(authentication)") 的意思就是 去testPermissionEvaluator这个bean里来执行check方法,这里需要注意check 方法必须返回值是boolean的因为这个是要给投票器投票的,这个我们以后会说

来看看我这个bean的代码吧

interface TestPermissionEvaluator {
boolean check(Authentication authentication);
}

@Service("testPermissionEvaluator")
public class TestPermissionEvaluatorImpl implements TestPermissionEvaluator {

public boolean check(Authentication authentication) {
//这里可以拿到登陆信息然后随便的去定制自己的权限 随便你怎么查询
//true就是过,false就是不过
System.out.println("进入了自定义的匹配器" + authentication);
return false;
}
}

这里要说一下spring security没有权限的时候默认返回的是页面。像我这样js掉用返回json就需要配置一下权限异常处理器了。只需要实现一个AccessDeniedHandler 的接口即可

@Component
@Slf4j
public class AccessDeniedAuthenticationHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;

public AccessDeniedAuthenticationHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}


@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
log.info("没有权限");
httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value());
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(e.getMessage()));
}
}

然后在configure 中这样配置下就行了

.authenticated().and().exceptionHandling().accessDeniedHandler(accessDeniedAuthenticationHandler)

然后我们启动项目验证一下

然后下一篇帖子我会说权限注解,那个是真的方便。

本博文是基于springboot2.x 和security 5 如果有什么不对的请在下方留言。