在介绍完SpringSecurity实现前后端分离认证之后,然后就是SpringSecurity授权,在阅读本文章之前可以先了解一下作者的上一篇文章SpringSecurity认证SpringSecurity实现前后端分离登录token认证详解_山河亦问安的博客-CSDN博客。
目录
1. 授权
1.1 权限系统的作用
1.2 授权基本流程
1.3 授权实现
1.3.1 限制访问资源所需权限
1.3.2 权限校验方法
1.3.3 自定义权限校验方法
1.4 自定义失败方案
1.5 代码更改
1. 授权
1.1 权限系统的作用
例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。
总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可)不通过前端,直接去发送请求来实现相关功能操作。
所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须基于所需权限才能进行相应的操作。
1.2 授权基本流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。然后设置我们的资源所需要的权限即可。
1.3 授权实现
1.3.1 限制访问资源所需权限
SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。 但是要使用它我们需要先开启相关配置。我们需要在springSecurity配置类上加下面这行代码:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
}
@EnableGlobalMethodSecurity 注解参数说明:
- prePostEnabled = true 会解锁 @PreAuthorize 和 @PostAuthorize 两个注解。顾名思义,@PreAuthorize 注解会在方法执行前进行验证,而 @PostAuthorize 注解会在方法执行后进行验证。
- securedEnabled = true 会解锁 @Secured 注解。@Secured 是专门用于判断是否具有角色的。能写在方法或类上。参数要以 ROLE_开头。
1.3.2 权限校验方法
hasAuthority(String)
判断用户是否具有特定的权限
@PostMapping("/test1")@PreAuthorize("hasAuthority('test1')")public String test1(){return "test";}
以上面代码为例,按Ctrl+Alt,然后点击上面的hasAuthority进入源码打断点如下图:
利用Apipost发送请求得到的结果如下图:
关键代码分析:
private boolean hasAnyAuthorityName(String prefix, String... roles) {Set<String> roleSet = this.getAuthoritySet(); //从SecurityContextHolder.getContext()中获取用户的权限集合String[] var4 = roles; //这是我们测试方法中hasAuthority中的权限信息test1int var5 = roles.length;for(int var6 = 0; var6 < var5; ++var6) {String role = var4[var6];String defaultedRole = getRoleWithDefaultPrefix(prefix, role); //遍历var4数组,其中的每个权限信息比如test1加上前缀,比如pre+test1,这里pre为null,如果roleset中包含这个权限信息就会返回true,该方法就会正常进行(roleSet.contains(defaultedRole)) {return true;}}return false;}
hasAnyAuthority(String …)
hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。
@PostMapping("/test")@PreAuthorize("hasAuthority('test1','test')")public String tes1t(){return "test";}
hasRole(String)
hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。
@PreAuthorize("hasRole('test')")public String hello(){return "hello";}
hasAnyAuthority(String …)
hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。
1.3.3 自定义权限校验方法
我们也可以定义自己的权限校验方法,在@PreAuthorize注解中使用我们的方法。代码如下:
@Component("ex")
public class SGExpressionRoot {public boolean hasAuthority(String authority){Authentication authentication = SecurityContextHolder.getContext().getAuthentication();MyUser myUser = (MyUser) authentication.getPrincipal();List<String> permissions = myUser.getTbUser().getPermissions();return permissions.contains(authority);}
}
在SPEL表达式中使用 @ex相当于获取容器中bean对象。然后再调用这个对象的hasAuthority方法,代码如下:
@PostMapping("/test")@PreAuthorize("@ex.hasAuthority('test')")public TbUser test(){TbUser tbUser = tbUserMapper.selectById(1);return tbUser;}
1.4 自定义失败方案
我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。 在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。 代码如下:
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {Result result = Result.error("401", "用户认证失败,请重新登录");response.setStatus(200);response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().print(result.toString());}
}
如果是授权过程中出现的异常会被封装成AccessDeniedException,然后调用AccessDeniedHandler对象的方法去进行异常处理。 代码如下:
@Component
public class AccessDenieHandleImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {Result result = Result.error("403", "用户权限不足");response.setStatus(200);response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().print(result.toString());}
}
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。配置代码如下:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {@Autowiredprivate AccessDenieHandleImpl accessDenieHandle;@Autowiredprivate AuthenticationEntryPointImpl authenticationEntryPoint;http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) //配置认证失败处理器.accessDeniedHandler(accessDenieHandle); //配置授权失败处理器http.cors() //允许跨域}}
1.5 代码更改
在认证的基础上添加授权,为了方便这里的权限信息设计写死,在真正的项目中每个用户的权限应该从数据库中进行查询。代码设计如下:
MyUser类代码如下:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MyUser implements UserDetails, Serializable {private TbUser tbuser;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {List<SimpleGrantedAuthority> collect = tbuser.getPermissions().stream().map(SimpleGrantedAuthority::new).distinct().collect(Collectors.toList());return collect;}@Overridepublic String getPassword() {return sysUser.getPassword();}@Overridepublic String getUsername() {return sysUser.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
JwtAuthenticationTokenFilter类代码如下:
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@AutowiredRedisTemplate redisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {//获取请求头中的tokenString token = httpServletRequest.getHeader("token");//如果token为空直接放行,由于用户信息没有存放在SecurityContextHolder.getContext()中所以后面的过滤器依旧认证失败符合要求if(!StringUtils.hasText(token)){filterChain.doFilter(httpServletRequest,httpServletResponse);return;}Long userId;try {//通过jwt工具类解析token获得userId,如果token过期或非法就会抛异常DecodedJWT decodedJWT = JwtUtil.decodeToken(token);userId = decodedJWT.getClaim("userId").asLong();}catch (Exception e){e.printStackTrace();throw new RuntimeException("token非法");}//根据userId从redis中获取用户信息,如果没有该用户就代表该用户没有登录过TbUser myUser = (TbUser) redisTemplate.opsForValue().get(String.valueOf(userId));if(Objects.isNull(myUser)){throw new RuntimeException("用户未登录");}MyUser myUser1=new MyUser(myUser);//将用户信息存放在SecurityContextHolder.getContext(),后面的过滤器就可以获得用户信息了。UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(myUser1,null,myUser1.getAuthorities());SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);filterChain.doFilter(httpServletRequest,httpServletResponse);}
}
UserDetailServiceImpl类
@Service
public class UserDetailServiceImpl implements UserDetailsService {@AutowiredTbUserMapper tbUserMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {QueryWrapper<TbUser> queryWrapper=new QueryWrapper<>();queryWrapper.eq("username",username);TbUser tbUser = tbUserMapper.selectOne(queryWrapper);List<String> list = Arrays.asList("test");tbUser.setPermissions(list);if(tbUser==null){throw new RuntimeException("用户名或者密码错误");}return new MyUser(tbUser);}
}
至此SpringSecurity实现前后端分离token验证认证授权就此结束。