• springSecurity登录的全过程


    登录

    为了更好的表达整个程序流程,以下顺序全部按照我编码思路流程进行

    登录思路

    思路:

    1. 请求登录接口,进入方法
      进入一下接口,loginBody中是登录专用实体类,里面只有username和password。
    	 /**
         * 登录方法
         *
         * @param loginBody 登录信息
         * @return 结果
         */
        @PostMapping("/login")
        public Res login(@RequestBody LoginBody loginBody) {
            System.out.println("登录"+loginBody);
            return sysUserService.login(loginBody);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. 用户验证
      在登录方法中,首先进行登录验证。至于为什么会进入UserDetailsService,请查看authenticationManager.authenticate()调用UserDetailsServiceImpl.loadUserByUsername过程
    //用户验证   该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            Authentication authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(loginBody.getUserName(), loginBody.getPassword()));
    
    • 1
    • 2
    • 3

    进入到UserDetailsService.loadUserByUsername()中,会返回UserDetails,UserDetails是用户的详细信息。
    此时要实现UserDetailsService接口的loadUserByUsername()方法去做一些,原因在下面的代码注释中。

    @Service
    @Slf4j
    public class UserDetailServiceImpl implements UserDetailsService {
        @Autowired
        private SysUserMapper sysUserMapper;
    
        /**
         * 定义自己的登陆逻辑不走安全框架的默认
         * 备注:
         * security拥有自己的登录界面和登录逻辑
         * 为了使用自己的登录逻辑,创建一个UserDetailServiceImpl实现框架的UserDetailsService
         * authenticationManager.authenticate()会调用次方法
         * @param username
         * @return UserDetails 为了返回用户信息,创建了一个登录类LoginUser继承了UserDetails
         * @throws UsernameNotFoundException
         */
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            SysUser user = sysUserMapper.selectOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUserName, username));
            if (user==null) {
                log.info("登录用户:{} 不存在.", username);
                throw new RuntimeException("登录用户:" + username + " 不存在");
            } else if ("2".equals(user.getDelFlag())) {
                log.info("登录用户:{} 已被删除.", username);
                throw new RuntimeException("对不起,您的账号:" + username + " 已被删除");
            } else if ("2".equals(user.getStatus())) {
                log.info("登录用户:{} 已被停用.", username);
                throw new RuntimeException("对不起,您的账号:" + username + " 已停用");
            }
            //这是权限,应该从数据库中获取的,为了方便使用,直接在这里写死了,后续使用中因该联合用户信息、角色、菜单查询
            Set<String> permissions=new HashSet<>();
            permissions.add("admin");
            //把获取到的登录用户信息添加到LoginUser返回
            LoginUser loginUser=new LoginUser(user.getUserId(),user.getDeptId(),user, permissions);
            System.out.println("UserDetailServiceImpl自己定义的登陆逻辑返回结果"+loginUser);
            //次方法中返回值是UserDetails,但最后返回的是LoginUser,是因为LoginUser实现了UserDetails类
            //注意这里的LoginUser不能是登录的实体类。因为会造成username的null指针异常。还有loginUser中重写的方法给默认值改成true;
            return loginUser;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    1. 获取用户信息
    
    	   //获取当前用户信息
            LoginUser loginUserInfo = (LoginUser) authentication.getPrincipal();
            System.out.println("登陆接口中用户验证结果" + loginUserInfo);
            // 生成token
            return Res.ok(createToken(loginUserInfo));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    从上一步验证中获取到登录用户信息。次方法可以封装成一个SecurityUtils安全服务工具类,用于获取用户的各种信息。

    1. 生成token信息返回给前端
      生成token的流程是,先生成一个随机数,把这个随机数作为key,登录用户信息作为value,存入到redis中,接着把token通过jwt进行加密,把加密过后的随机数返回给前端。在前端进行其他请求的时候,前端在请求头中把加密过后的token传给后台,后台通过jwt解析,把redis中存的key解析出来,再去redis获取用户信息。
     public String createToken(LoginUser loginUserInfo) {
            //获取一个随机数token
            String token = UUID.randomUUID().toString();
            loginUserInfo.setToken(token);
            //将token作为key,loginUserInfo作为value,存入redis
            redisTemplate.opsForValue().set("login_user_key"+token, loginUserInfo);
            Map<String, Object> claims = new HashMap<>();
            claims.put("uuid", token);
            //将生成的随机数进行加密返回给前端,后续过程中,前端会通过传这个个加密之后的token1,然后解密出token随机数,然后去redis中取出用户信息
            String token1 = Jwts.builder()
                    .setClaims(claims)
                    .signWith(SignatureAlgorithm.HS512, "myruoyiitem").compact();
            System.out.println("生成token随机数" + token);
            System.out.println("对token随机数进行加密" + token1);
            return token1;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    总结

    整个流程就是:进入接口—>进行验证—>获取用户信息—>创建token—>存入redis—>返回

    注意点

    登录所用的实体和实现UserDetails的实体不能是一个,具体的空指针我忘记在哪里出现了,反正遇到这个问题了。

    添加认证

    认证的目的

    目的就是让用户请求的时候,携带加密之后的随机数(token),去验证用户的信息

    认证过程

    1. 创建认证过滤器JwtAuthenticationTokenFilter,记得实现OncePerRequestFilter。下面获取token的过程就是获取请求头token,然后解密出token随机数,然后去redis中取出用户信息
    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
        @Autowired
        TokenUtil tokenUtil;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            System.out.println("对于在SecurityConfig中没有放行的都进入到这个接口");
            //获取token
            LoginUser user = tokenUtil.getLoginUser(request);
            if (user != null) {
                //验证令牌有效期,相差不足20分钟,自动刷新缓存,预防三十分钟之后自动失效
                //tokenUtil.verifyToken(user);
                //TODO 获取权限信息封装到Authentication中
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
            //放行
            filterChain.doFilter(request, response);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    1. 创建完成之后,把JwtAuthenticationTokenFilter加入到SecurityConfig过滤器链中
    /**
     * SecurityConfig设置
     * @author lenovo
     * @date 2022/6/20
     * @EnableGlobalMethodSecurity注解prePostEnabled开启注解功能
     *
     */
    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        /**
         * 自定义用户认证逻辑
         */
        @Autowired
        private UserDetailsService userDetailsService;
        /**
         * token认证过滤器
         */
        @Autowired
        JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
        /**
         * 认证失败处理类
         */
        @Autowired
        private AuthenticationEntryPointImpl unauthorizedHandler;
        /**
         * 授权失败处理类
         */
        @Autowired
        private AccessDeniedHandlerImpl accessDeniedHandler;
    
        // 配置用于放行login页面
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //关闭csrf
            http.csrf().disable()
                    // 认证失败、授权失败处理类
                    .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedHandler(accessDeniedHandler).and()
                    //不通过Session获取SecurityContext
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .authorizeRequests()
                    // 对于登录接口 允许匿名访问
                    .antMatchers("/login", "/register").anonymous()
                    .antMatchers("/login", "/register").permitAll()
                    // 除上面外的所有请求全部需要鉴权认证
                    .anyRequest().authenticated();
            //把token校验过滤器添加到过滤器链中
            http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    
        }
    
        /**
         * 将AuthenticationManager作为对象注入容器
         */
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        /**
         * 把PasswordEncoder注入bean
         * 加密方法
         *
         * @return
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        /**
         * 身份认证接口
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    备注
    1. 为什么设置.antMatchers(“/login”,“/register”).anonymous()?
    此时,用户登录的时候就会首先进入JwtAuthenticationTokenFilter,因为没有token,直接放行,但是在SecurityConfig中,并不会放行,请求会返回状态码401,没有认证成功。但是如果设置.antMatchers(“/login”,“/register”).anonymous()之后,请求会放行登录注册接口。

    处理认证失败的请求

    上面说到,请求认证失败之后会返回给前端一个状态码401,此时,我们需要做一下异常处理。来处理一下这个401状态,使返回的状态使是200,code=401

    /**
     * 认证失败处理类
     *
     * @author ruoyi
     */
    @Component
    public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
        private static final long serialVersionUID = -8970718410437077606L;
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
                throws IOException {
            int code = 401;
            String msg = "请求访问:{" + request.getRequestURI() + "},认证失败,无法访问系统资源";
            try {
                response.setStatus(200);
                response.setContentType("application/json");
                response.setCharacterEncoding("utf-8");
                response.getWriter().print(JSON.toJSONString(Res.failed(code,msg)));
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    处理之后并不是就这样能生效,而是还需要再SecurityConfig配置这个异常处理

    http.csrf().disable()
                    // 认证失败、授权失败处理类
                    .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
    
    • 1
    • 2
    • 3

    这样才能生效。

    添加授权

    一般我们都是使用注解的方式去进行授权。

    在请求带 @PreAuthorize(“@ss.hasPermi(‘admin’)”)的接口中。都进行授权。也需要一个授权的服务。注意注解ss是服务中名称。

     /**
         * 测试token
         *
         * @return 结果
         */
        @GetMapping("/testToken")
        @PreAuthorize("@ss.hasPermi('admin')")
        public Res testToken() {
            System.out.println("测试token");
            return Res.ok("测试通过");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    自定义权限实现

    /**
     * 自定义权限实现,ss取自SpringSecurity首字母
     *
     * @author lenovo
     * @date 2022/6/21
     */
    @Service("ss")
    public class PermissionService {
        /**
         * 所有权限标识
         */
        private static final String ALL_PERMISSION = "*:*:*";
    
        /**
         * 管理员角色权限标识
         */
        private static final String SUPER_ADMIN = "admin";
    
        private static final String ROLE_DELIMETER = ",";
    
        private static final String PERMISSION_DELIMETER = ",";
    
        /**
         * 验证用户是否具备某权限
         *
         * @param permission 权限字符串
         * @return 用户是否具备某权限
         */
        public boolean hasPermi(String permission) {
            System.out.println("拥用这个权限才能通过"+permission);
            if (StringUtils.isEmpty(permission)) {
                return false;
            }
            LoginUser loginUser = SecurityUtils.getLoginUser();
            System.out.println("当前用户信息"+loginUser);
            if (loginUser==null|| CollectionUtils.isEmpty(loginUser.getPermissions())) {
                return false;
            }
            return hasPermissions(loginUser.getPermissions(), permission);
        }
        /**
         * 判断是否包含权限
         *
         * @param permissions 权限列表
         * @param permission 权限字符串
         * @return 用户是否具备某权限
         */
        private boolean hasPermissions(Set<String> permissions, String permission)
        {
            return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    重点:配置完成之后,也别注意,此时注解还不能使用,需要在SecurityConfig开启注解。再SecurityConfig类加上注解开启
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)

    处理授权失败异常

    /**
     * 授权失败处理类
     * @author lenovo
     * @date 2022/6/21
     */
    @Component
    public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            int code = 403;
            String msg = "请求访问:{" + request.getRequestURI() + "},失败,权限不足";
            try {
                response.setStatus(200);
                response.setContentType("application/json");
                response.setCharacterEncoding("utf-8");
                response.getWriter().print(JSON.toJSONString(Res.failed(code,msg)));
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这样,如果授权失败,请求返回的是一个403状态,根据认证失败的处理过程,同理授权页需要再SecurityConfig配置授权

    http.csrf().disable()
                    // 认证失败、授权失败处理类
                    .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedHandler(accessDeniedHandler).and()
    
    • 1
    • 2
    • 3

    注册注意的点

    在使用注册的时候,加密方式一定要和登录相同,
    数据库添加用户信息时,密码一定要生成BCryptPasswordEncoder密码再添加。

  • 相关阅读:
    ssm分页实战
    Jenkins服务开机自启动
    Linux计划任务以及进程检测与控制
    Kubernetes Dashboard 部署应用以及访问
    基于AI算法的5G多接入协同方案及关键技术
    用UNIGUI实现的网络共享打印(云打印)效果
    stable diffusion汉化
    安全好用性价比高的远程协同运维软件有吗?
    Win10+Ubuntu20.04双系统双硬盘(SSD+HDD)安装与启动
    Jenkins对应java版本
  • 原文地址:https://blog.csdn.net/XuDream/article/details/125390101