美文网首页
Spring Security前后端分离项目并采用Token持久

Spring Security前后端分离项目并采用Token持久

作者: 光芒121 | 来源:发表于2021-07-14 21:51 被阅读0次

    最近一直在研究把公司的后端项目登录模块切换成Spring自家的Security安全框架,说下我们现有的登录模块,手机号+验证码登录,登录成功生成一个返回用户信息里面包含Authentication返回给前端,前端下一次请求接口时候Header中携带Authentication,以便后端根据Authentication去Redias中查询用户登录信息是否有效或者是否登录成功;目前的登录模式应该也是大多公司采用的模式。

    当我研究使用Security时候发现其实这个框架并不适合这种前后端分离、无状态请求结构项目,为什么这么说呢,Security自家框架本身是采用Session会话管理机制,也就是你登录成功了,下一次请求时候是采用Session去判断你是否登录成功,否则框架是不会让你进到框架里面的,Controller里面的接口方法都进不去,框架直接给拦截了;那么想继续采用Security自家的也要保持现有的也就是我们公司的登录模式也是可以的,我感觉没必要,看完大家应该也会感觉没必要,下面直接上代码,看完你们在评论是否有必要在现有模式上使用Security安全框架。
    项目中注意点:
    1、登录是使用Controller中的登录,没有使用Security自带的formLogin登录方法,框架的传参是固定的username、password 2个字段,如果登录有多参传递或者参数名字不一致,那就需要重写关于参数方法的class了,所以我们采用Controller中自己的方法块。
    2、采用Token持久化,需要写一个拦截器,也就是在你登录成功后请求下一个接口时候不被框架拦截,那么就需要自定义一个拦截器,下一次请求时候要跑在 框架拦截器前面,重新设置下SecurityContextHolder.getContext().setAuthentication(...),否则就被框架拦在外面了,也就是会到框架的没登录回调方法里面了。

        @ApiOperation("登录")
        @PostMapping(value = "/login")
        public ResponseEntity<UserInfoVO> login(@Validated @RequestBody LoginUserLoginVO loginUserLoginVO) throws Exception {
            UserInfoVO userInfo = authService.login(loginUserLoginVO);
            return new ResponseEntity<>(userInfo, HttpStatus.OK);
        }
    
      //业务实现类
        @Service
        public class AuthServiceImpl implements AuthService {
        @Override
        public RetLoginUserInfoVO login(LoginUserLoginVO loginUserLoginVO) throws JsonProcessingException {
            // 根据手机号判断登陆
            User userInfoVO = authMapper.getUserInfoByPhone(loginUserLoginVO.getPhone());
            .......省略判断是否登录逻辑
              //Security 框架设置 Authentication
            //第三个参数传null是因为这个参数是权限参数,我们项目没有用到权限,也就是只有admin才能访问哪些类、只有user用户能访问哪些类设置
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userInfoVO.getPhone(), userInfoVO.getToken(), null);
            Authentication authenticate = authenticationManager.authenticate(authenticationToken);
            SecurityContextHolder.getContext().setAuthentication(authenticate);
            return userInfoVO;
          }
        }
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        //登录成功、登录失败、没有登录 回调类
        private final RequestCallbackHandler callbackHandler;
        private final OnceFilter  filter;
        public SecurityConfig(RequestCallbackHandler callbackHandler,OnceFilter  filter) {
            this.callbackHandler = callbackHandler;
            this.filter = filter;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //自定义的那个拦截器
            http.addFilterAfter(this.filter, UsernamePasswordAuthenticationFilter.class);
    
            http.headers().cacheControl();
            http.cors().and().csrf().disable();
            //关掉 session会话管理
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            //关掉 Security 自带的login
            http.httpBasic().disable();
            http.authorizeRequests()
                    //可不用登录进行访问的接口设置
                    .antMatchers("/auth/login", "/auth/code")
                    .permitAll()
                    //其他接口都需要验证
                    .anyRequest()
                    .authenticated()
                    .and()
                    //注销
                    .logout().permitAll()
                    .logoutSuccessHandler(callbackHandler)
                    //配置回调接口
                    .and().exceptionHandling()
                    .accessDeniedHandler(callbackHandler)
                    //没有登录就请求其他接口就会回调 commence 提示没有请先登录在访问
                    .authenticationEntryPoint(callbackHandler);
        }
    }
    
    //回调类
    @Component
    public class RequestCallbackHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler,
            AuthenticationEntryPoint, AccessDeniedHandler, LogoutSuccessHandler {
    
        private static final Logger log = LoggerFactory.getLogger(AuthServiceImpl.class);
    
        /**
         * 登录成功
         * 属于 AuthenticationSuccessHandler 的接口
         */
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            log.info("【使用Security登录成功回调处】登录成功");
        }
    
        /**
         * 登录失败
         * 属于 AuthenticationFailureHandler 的接口
         */
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            setResponseErrorMessage(exception.getMessage(), HttpStatus.UNAUTHORIZED, response);
            log.info("【Security登录失败回调处】登录失败:{}", request.getRequestURI() + " -- " + exception.getMessage());
        }
    
        /**
         * 没有登录回调
         * 属于 AuthenticationEntryPoint 的接口
         */
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
            log.info("【Security没有登录回调】请先登录:{}", request.getRequestURI());
            setResponseErrorMessage("请先登录", HttpStatus.UNAUTHORIZED, response);
        }
    
        /**
         * 无权限访问
         * 属于 AccessDeniedHandler 的接口
         */
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            log.info("【Security没有权限请求回调】无权访问,请联系管理员:{}", request.getRequestURI());
        }
    
        /**
         * 退出登录成功
         * 属于 LogoutSuccessHandler 的接口
         */
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            log.info("【Security退出登录成功回调】退出登录成功:{}", request.getRequestURI());
        }
    
        /**
         * 异常时候返回给用户端的消息
         */
        public void setResponseErrorMessage(String message, HttpStatus status, HttpServletResponse response) throws IOException {
            ResponseEntity<Failure> bodyEntity = new ResponseEntity<>(new Failure(message), status);
            Failure body = bodyEntity.getBody();
            //对象转json反给用户端
            String s = FabsBeanUtils.toJsonString(body);
            response.setStatus(status.value());
            response.getWriter().print(s);
        }
    }
    

    最后自定义的拦截器类了

    @Component
    public class OnceFilter extends OncePerRequestFilter {
        
        private final AuthService authService;
        private final RequestCallbackHandler handler;
        public OnceFilter(AuthService authService, RequestCallbackHandler handler) {
            this.authService = authService;
            this.handler = handler;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
                String token = request.getHeader("Authorization");
                // 校验用户是否登录
                User userInfo = null;
                try {
                    //根据传递的headle中的token去读取redias中是否过期和是否有用户信息
                    userInfo = authService.authentication(token);
                } catch (Exception e) {
                    e.printStackTrace();
                    handler.setResponseErrorMessage(e.getMessage(), HttpStatus.UNAUTHORIZED, response);
                    return;
                }
                if (userInfo != null) {
                    //为了保持持久化登录重新设置 security 的 authentication 登录信息验证
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userInfo.getPhone(), userInfo.getId(), null);
                    authenticationToken.setDetails(userInfo);
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
            filterChain.doFilter(request, response);
    }
    

    大工告成。。。

    讨论:如果不采用Security安全框架,自己也去写一个拦截框架,每次请求接口去判断headle中是否有传参,再去redias中去读取用户信息也是可以的,所以这就是我感觉Security不适合我们这种登录模式,适合那种前后端不分离、只给web前端使用的项目,我们项目要给App去使用,那App每次调用接口肯定都是无状态的、不会记住session的,所以没用到Security本身的拦截机制,没体验到框架的自身的安全便利性。

    下一期分享一个使用Security框架自身的formLogin登录,并使用接收验证码登录验证的项目。

    相关文章

      网友评论

          本文标题:Spring Security前后端分离项目并采用Token持久

          本文链接:https://www.haomeiwen.com/subject/oajupltx.html