Spring Security具有强大的权限验证。
权限有些人认为是页面的隐藏,其实不然。权限可以理解为是否可以访问资源,页面隐藏什么的是客户友好度的事情,所以对于web而言,系统的安全不安全,最终取决于对url的控制。
本文章代码可以参考
https://gitee.com/Maoxs/security-test
中的 security-permission
首先呢是登陆
<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
@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 如果有什么不对的请在下方留言。