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中的权限信息test1 int 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 { @Override public 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 { @Override public 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()); } }
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoin
t
和AccessDeniedHandler
然后配置给SpringSecurity
即可。配置代码如下:
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private AccessDenieHandleImpl accessDenieHandle; @Autowired private 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; @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> collect = tbuser.getPermissions().stream().map(SimpleGrantedAuthority::new).distinct().collect(Collectors.toList()); return collect; } @Override public String getPassword() { return sysUser.getPassword(); } @Override public String getUsername() { return sysUser.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
JwtAuthenticationTokenFilter
类代码如下:
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired RedisTemplate redisTemplate; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { //获取请求头中的token String 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 { @Autowired TbUserMapper tbUserMapper; @Override public 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
验证认证授权就此结束。