我们不用SpringSecurity自带的认证授权。

1.登陆接口Controller

接下我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。
在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。

  1. package com.lyd.springsecurity1.controller;
  2. @RestController
  3. public class LoginController {
  4. //具体的认证,生成jwt,存入redis的方法,写到service层,LoingService是自己写的Service接口
  5. @Autowired
  6. private LoginService loginService;
  7. //这个是登陆接口
  8. @PostMapping ("/login")
  9. public ResponseResult login(@RequestBody User user){
  10. return loginService.login(user);
  11. }
  12. //这个是登出接口,登出的时候不需要什么参数,请求头带上token就行
  13. @GetMapping ("/logout")
  14. public ResponseResult login(){
  15. return loginService.logout();
  16. }
  17. }

2.SpringSecurity配置类

  1. package com.lyd.springsecurity1.config;
  2. //限制访问资源需要的权限,就是访问控制器需要验证权限,开启这个功能
  3. @EnableGlobalMethodSecurity(prePostEnabled = true)
  4. @Configuration
  5. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  6. //创建BCryptPasswordEncoder注入容器
  7. //这个是用来验证加密密码的,可以自己写一个自己的加密方式
  8. @Bean
  9. public PasswordEncoder passwordEncoder(){
  10. return new BCryptPasswordEncoder();
  11. }
  12. //这个是用来在service层验证用户的,通过AuthenticationManager的authenticate方法来进行用户认证
  13. @Override
  14. @Bean
  15. public AuthenticationManager authenticationManagerBean() throws Exception {
  16. return super.authenticationManagerBean();
  17. }
  18. //token验证的过滤器
  19. //我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。使用userid去redis中获取对应的LoginUser对象。然后封装Authentication对象存入SecurityContextHolder
  20. @Autowired
  21. private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
  22. //注入认证异常处理和授权异常处理
  23. @Autowired
  24. private AuthenticationEntryPoint authenticationEntryPoint;
  25. @Autowired
  26. private AccessDeniedHandler accessDeniedHandler;
  27. //配置
  28. @Override
  29. protected void configure(HttpSecurity http) throws Exception {
  30. http
  31. //关闭csrf
  32. .csrf().disable()
  33. //不通过Session获取SecurityContext
  34. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
  35. .and()
  36. .authorizeRequests()
  37. // 对于登录接口 允许匿名访问,这个antMatchers()可以写多个,链式编程
  38. .antMatchers("/login").anonymous()
  39. // 除上面外的所有请求全部需要鉴权认证
  40. .anyRequest().authenticated();
  41. //禁止默认的登出
  42. http.logout().disable();
  43. //允许跨域
  44. http.cors();
  45. //添加一个过滤器jwtAuthenticationTokenFilter,添加在UsernamePasswordAuthenticationFilter过滤器之前
  46. //jwtAuthenticationTokenFilter是自己写的过滤器
  47. http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);
  48. //配置异常处理器
  49. http.exceptionHandling()
  50. .authenticationEntryPoint(authenticationEntryPoint)//配置认证异常处理器
  51. .accessDeniedHandler(accessDeniedHandler);//配置授权异常处理器
  52. }
  53. }

3.Service层认证用户

接口

  1. package com.lyd.springsecurity1.service;
  2. public interface LoginService {
  3. //User是用户的实体类
  4. ResponseResult login(User user);
  5. ResponseResult logout();
  6. }

实现这个接口。规范

  1. package com.lyd.springsecurity1.service;
  2. @Service
  3. public class LoginServiceImpl implements LoginService{
  4. //这个在配置文件中注入过了,用来进行用户认证
  5. @Autowired
  6. private AuthenticationManager authenticationManager;
  7. //把Redis的基本操作都放在RedisCache工具类中,直接调用方便
  8. @Autowired
  9. private RedisCache redisCache;
  10. @Override
  11. public ResponseResult login(User user) {
  12. //看:注解1
  13. UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
  14. Authentication authenticate = authenticationManager.authenticate(authenticationToken);
  15. //如果认证没通过,给出对应的提示,抛出异常,交给全局异常捕获,认证没通过authenticate就是null
  16. if(Objects.isNull(authenticate)){
  17. throw new RuntimeException("登陆失败");
  18. }
  19. //如果认证通过了,authenticate不是null,使用userid生成一个jwt,jwt存入返回结果里面,返回出去
  20. //LoginUser是自己写的登陆用户类
  21. LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
  22. String userid = loginUser.getUser().getId().toString();
  23. //这里用的JWT工具类
  24. String jwt = JwtUtil.createJWT(userid);
  25. //把完整的用户信息存入Redis,usrid作为key
  26. redisCache.setCacheObject("login:"+userid,loginUser);
  27. Map<String,String> map = new HashMap<>();
  28. map.put("token",jwt);
  29. //返回统一格式的响应体
  30. return new ResponseResult(200,"登陆成功",map);
  31. }
  32. //登出
  33. @Override
  34. public ResponseResult logout() {
  35. //获取SecurityContextHolder中的用户id
  36. UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
  37. LoginUser loginUser = (LoginUser) authentication.getPrincipal();
  38. String id = loginUser.getUser().getId().toString();
  39. //删除redis中的值
  40. //如果两个人同时登陆一个账号,其中一个人退出登陆了,如果用id作为键的话,另外一个人也被迫退出了,可以用uuid之类的
  41. redisCache.deleteObject("login:"+id);
  42. return new ResponseResult(200,"退出成功");
  43. }
  44. }

:::warning 注解1:
调用AuthenticationManager的authenticate方法进行用户认证,这个在Security配置里面注入的。
而authenticate方法传入一个Authentication类型的对象,传入它的子类UsernamePasswordAuthenticationToken,把前端传入的账号密码放进去。
这个authenticate就完成了登陆时候的验证?账号密码在数据库里面的啊,所以这个方法一定包含了去数据库查询账号密码验证的一系列步骤,甚至LoginUser这个我自己写的对象也在里面。
所以还有个自定义东西需要我们去完成查数据库,LoginUser封装等。我们写好后替代原生的方法,就是把写好的东西放入容器,运行的时候authenticate就用我们自己写的那个东西。
SpringSecurity认证
看认证流程图 :::

4.实现UserDetailsService

根据认证流程图,我们需要自定义的去数据库查询用户信息,创建一个类实现UserDetailsService接口,重写其中的loadUserByUsername方法。

  1. package com.lyd.springsecurity1.service;
  2. //自己实现一个UserDetailsService,重写loadUserByUsername方法
  3. //这里自定义的验证
  4. @Service
  5. public class UserDetailsServiceImpl implements UserDetailsService {
  6. @Autowired
  7. private UserMapper userMapper;
  8. @Override
  9. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  10. //根据用户名查询用户
  11. User user = userMapper.queryUser(username);
  12. //如果查出来的user为空,就抛出异常,SpringSecurity里面有个过滤器可以监视其他过滤器抛出的异常
  13. if(Objects.isNull(user)){
  14. throw new UsernameNotFoundException("用户名没有找到");
  15. }
  16. //查询对应的权限信息
  17. List<String> permissionKeyList = userMapper.queryPermissions(user.getId());
  18. //最终要把用户信息,和权限信息,封装成UserDetails返回。这是一个接口,需要自己写一个实现类实现这个接口
  19. return new LoginUser(user,permissionKeyList);
  20. }
  21. }

5.实现UserDetails

这个是用来装用户数据的,是要给接口,我们要实现这个接口

  1. package com.lyd.springsecurity1.domain;
  2. public class LoginUser implements UserDetails {
  3. //自己定义用户属性
  4. private User user;
  5. //存储权限信息,权限信息就是一个字符串列表,一个字符串一个权限
  6. private List<String> permissions;
  7. public LoginUser() {
  8. }
  9. public LoginUser(User user, List<String> permissions) {
  10. this.user = user;
  11. this.permissions = permissions;
  12. }
  13. public List<String> getPermissions() {
  14. return permissions;
  15. }
  16. public void setPermissions(List<String> permissions) {
  17. this.permissions = permissions;
  18. }
  19. public User getUser() {
  20. return user;
  21. }
  22. public void setUser(User user) {
  23. this.user = user;
  24. }
  25. //存储SpringSecurity所需要的权限信息的集合
  26. //这个集合在序列化时,不存入redis
  27. @JSONField(serialize = false)
  28. private List<GrantedAuthority> authorities;
  29. /*
  30. * 最终认证都是通过UserDetails这个类的下面这些方法进行认证,比如获取账号密码什么的
  31. * 你可以自己添加一个其他的
  32. * 所以这些方法都是根据上面的user对象里面的属性,而得到返回结果
  33. * */
  34. @Override
  35. //得到权限信息
  36. public Collection<? extends GrantedAuthority> getAuthorities() {
  37. if(authorities!=null){
  38. return authorities;
  39. }
  40. //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
  41. for(String p:permissions){
  42. authorities.add(new SimpleGrantedAuthority(p));
  43. }
  44. return authorities;
  45. }
  46. @Override
  47. //得到用户密码
  48. public String getPassword() {
  49. return user.getPassword();
  50. }
  51. @Override
  52. //得到用户名
  53. public String getUsername() {
  54. return user.getUserName();
  55. }
  56. @Override
  57. //是否没有过期
  58. public boolean isAccountNonExpired() {
  59. return true;
  60. }
  61. @Override
  62. //是否没有被锁定
  63. public boolean isAccountNonLocked() {
  64. return true;
  65. }
  66. @Override
  67. //是否没有超时
  68. public boolean isCredentialsNonExpired() {
  69. return true;
  70. }
  71. @Override
  72. //是否可用
  73. public boolean isEnabled() {
  74. return true;
  75. }
  76. }

6.过滤器认证token

我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。
使用userid去redis中获取对应的LoginUser对象。
然后封装Authentication对象存入SecurityContextHolder

  1. package com.lyd.springsecurity1.filter;
  2. @Component
  3. public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
  4. //redis工具类
  5. @Autowired
  6. private RedisCache redisCache;
  7. @Override
  8. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  9. //获取token,这个是请求头里面是啥就是啥,请求头不一定就是token
  10. String token = request.getHeader("token");
  11. if(!StringUtils.hasText(token)){
  12. //请求头里面token都没有,那只有放行了.因为后面的过滤器会判断是不是认证状态,有可能登陆的时候没有token
  13. filterChain.doFilter(request,response);
  14. return;
  15. }
  16. //解析token,需要jwt的依赖和工具类
  17. String userid;
  18. try {
  19. Claims claims = JwtUtil.parseJWT(token);
  20. userid = claims.getSubject();
  21. } catch (Exception e) {
  22. //把数据直接响应给前端,用了WebUtils工具类,看准备工作那一章
  23. ResponseResult result = ResponseResult.errorResult(401, "token非法请重新登陆");
  24. WebUtils.renderString(response, JSON.toJSONString(result));
  25. return;
  26. }
  27. //从redis中获取用户信息
  28. String redisKey = "login:"+userid;
  29. LoginUser loginUser = redisCache.getCacheObject(redisKey);
  30. if(Objects.isNull((loginUser))){
  31. //redis中没有获取这个用户的信息,要么过期了要么一开始就没有这个数据,统一就是用户没有登陆
  32. ResponseResult result = ResponseResult.errorResult(401, "用户未登录请重新登陆");
  33. WebUtils.renderString(response, JSON.toJSONString(result));
  34. }
  35. //获取权限信息封装到Authentication中
  36. //在第3步和第4步,实现UserDetailsService和UserDetails中,好像设置了权限。下面就不用传入权限了,loginUser里面有。
  37. //存入SecurityContextHolder
  38. if (Objects.nonNull(loginUser)) {
  39. //这个玩意上面有提到
  40. UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
  41. SecurityContextHolder.getContext().setAuthentication(authenticationToken);
  42. }
  43. //token解析完了,就要放行了,进下一个过滤器,token解析查出来的用户数据,存放在一个公共的SecurityContextHolder里面,其他过滤器可以拿到
  44. filterChain.doFilter(request,response);
  45. }
  46. }

7.限制访问资源所需要的权限

在每个控制器上加上注解@PreAuthorize
里面的参数:
SpringSecurity自定义认证授权 - 图1

  1. package com.lyd.springsecurity1.controller;
  2. @RestController
  3. public class HelloController {
  4. //表示需要test权限才能访问这个接口
  5. @PreAuthorize("hasAuthority('test')")
  6. @GetMapping("/hello")
  7. public String hello(){
  8. return "返回你一个hello";
  9. }
  10. }

8.请求异常处理

我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。
要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。 :::success 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。 ::: 所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPointAccessDeniedHandler然后配置给SpringSecurity即可。
配置在第3步配置类里面有写

  1. package com.lyd.springsecurity1.handler;
  2. //关于认证异常的处理
  3. @Component
  4. public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
  5. @Override
  6. public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
  7. //如果异常了,会返回非200的响应吗,不返回内容。我们要统一返回一个响应格式,把失败的信息也返回,ResponseResult是自己定义的响应体
  8. ResponseResult result =new ResponseResult(401,"用户认证失败");
  9. //自己写了一个工具类,设置响应,在springsecurity准备工作里面这个工具类
  10. WebUtils.renderString(response,JSON.toJSONString(result));
  11. }
  12. }
  1. package com.lyd.springsecurity1.handler;
  2. //关于授权异常处理
  3. @Component
  4. public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
  5. @Override
  6. public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
  7. ResponseResult result =new ResponseResult(403,"你的权限不足");
  8. WebUtils.renderString(response, JSON.toJSONString(result));
  9. }
  10. }