• SpringBoot实践(二十六):Security实现Vue-Element-Admin登录拦截


    之前研究时候用mock.js和模拟登录接口完成Vue-Element-Admin的登录,生产很多场景会使用SpringSecurity框架,因为它功能较多,文档写的不错,所以现在使用真实的场景实现集成,security使用系列的过滤链完成请求的过滤和自定义,直接处理的是HttpServletResponse和HttpServletRequest对象。

    配置类

    EnableWebSecurity组合注解,包括  @Configuration,@EnableGlobalAuthentication,  @Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class,OAuth2ImportSelector.class })说明是个配置类,并且要创建哪些bean,WebSecurityConfigurerAdapter适配器模式表明它默认有些配置,你只需要重写自己那部分配置即可,这里使用以下几个核心对象:

    LoginAuthProvider:鉴权

    JsonWebTokenUtil :支持登录的post

    IRefreshTokenService:token刷新

    RefreshTokenMapper:token的数据库操作

    UserServiceImpl:用户校验

    JsonWebTokenProperty:jwt自定义

     使用方式如下:

    1. @EnableWebSecurity
    2. public class MasSecurity extends WebSecurityConfigurerAdapter {
    3. private static final Logger logger = LoggerFactory.getLogger(MasSecurity.class);
    4. @Autowired
    5. LoginAuthProvider loginAuthProvider;
    6. @Autowired
    7. JsonWebTokenUtil jsonWebTokenUtil;
    8. @Autowired
    9. IRefreshTokenService refreshTokenService;
    10. @Autowired
    11. RefreshTokenMapper refreshTokenMapper;
    12. @Autowired
    13. UserServiceImpl userService;
    14. @Autowired
    15. JsonWebTokenProperty jsonWebTokenProperty;
    16. // 先认证后鉴权
    17. @Override
    18. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    19. auth.authenticationProvider(loginAuthProvider);
    20. }
    21. @Override
    22. protected void configure(HttpSecurity http) throws Exception {
    23. //允许跨域,配置后SpringSecurity会自动寻找name=corsConfigurationSource的Bean
    24. http.cors();
    25. http.csrf().disable();
    26. //当访问接口失败的配置
    27. http.exceptionHandling().authenticationEntryPoint(new InterfaceAccessException());
    28. http.authorizeRequests()
    29. .antMatchers("/","/login", "/swagger-ui.html","/swagger-resources/**","/webjars/**","/v2/**","/api/**").permitAll()
    30. .anyRequest().authenticated()
    31. .and()
    32. .formLogin().loginProcessingUrl("/login")
    33. .and().addFilterAt(jsonAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
    34. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//因为用不到session,所以选择禁用
    35. http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler()).deleteCookies(jsonWebTokenProperty.getHeader()).clearAuthentication(true);
    36. http.addFilterAfter(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    37. }
    38. @Bean(name="myRequestMatcher")
    39. public SkipPathAntMatcher skipPathAntMatcher(){
    40. List uris = new LinkedList<>();
    41. uris.add("/login");
    42. uris.add("/swagger-resources/**");
    43. uris.add("/webjars/**");
    44. uris.add("/v2/**");
    45. uris.add("/api/**");
    46. uris.add("/swagger-ui.html");
    47. return new SkipPathAntMatcher(uris);
    48. }
    49. @Bean
    50. public JwtAuthenticationFilter jwtAuthenticationFilter() {
    51. return new JwtAuthenticationFilter(jsonWebTokenUtil, userService,skipPathAntMatcher(), refreshTokenMapper);
    52. }
    53. @Bean
    54. public JsonAuthenticationFilter jsonAuthenticationFilter() throws Exception {
    55. JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
    56. filter.setAuthenticationManager(authenticationManagerBean());
    57. filter.setFilterProcessesUrl("/login");
    58. filter.setAuthenticationSuccessHandler(new MySuccessHandler());
    59. filter.setAuthenticationFailureHandler(new MyFailHandler());
    60. return filter;
    61. }
    62. @Bean
    63. public LogoutSuccessHandler logoutSuccessHandler() {
    64. return new LogoutSuccessHandler() {
    65. @Autowired
    66. public void setObjectMapper(ObjectMapper objectMapper) {
    67. this.objectMapper = objectMapper;
    68. }
    69. private ObjectMapper objectMapper;
    70. @Override
    71. public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    72. response.setContentType("application/json;charset=utf-8");
    73. PrintWriter out = response.getWriter();
    74. out.write(objectMapper.writeValueAsString(ResponseData.success("logout success.")));
    75. out.flush();
    76. out.close();
    77. }
    78. /* // 这种写法更简洁
    79. @Override
    80. public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    81. response.setContentType("application/json;charset=utf-8");
    82. PrintWriter out = response.getWriter();
    83. out.write(new ObjectMapper().writeValueAsString(ResponseData.success("logout success.")));
    84. out.flush();
    85. out.close();*/
    86. };
    87. }
    88. //登录成功的处理类
    89. class MySuccessHandler implements AuthenticationSuccessHandler {
    90. @Override
    91. public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
    92. Authentication authentication) throws IOException, ServletException {
    93. UserDetails details = (UserDetails) authentication.getPrincipal();
    94. List roles = (List) details.getAuthorities();
    95. //登录时同时生成refreshToken,保存到表中
    96. logger.info("begin to onAuthenticationSuccess.");
    97. RefreshTokenEntity token = new RefreshTokenEntity();
    98. token.setUsename(details.getUsername());
    99. String refreshToken = jsonWebTokenUtil.generateRefreshToken(details, roles.get(0).getAuthority());
    100. token.setToken(refreshToken);
    101. LambdaQueryWrapper queryWrapper = new QueryWrapper().lambda().eq(RefreshTokenEntity::getUsename, details.getUsername());
    102. RefreshTokenEntity refreshTokenTemp = refreshTokenMapper.selectOne(queryWrapper);
    103. if (refreshTokenTemp != null) {
    104. refreshTokenTemp.setToken(refreshToken);
    105. refreshTokenMapper.update(refreshTokenTemp, queryWrapper);
    106. } else {
    107. logger.info("begin to insert token");
    108. refreshTokenMapper.insert(token);
    109. logger.info("end to insert token");
    110. }
    111. response.setHeader(jsonWebTokenUtil.getHeader(), refreshToken);
    112. /* Cookie cookie = new Cookie("token", refreshToken);
    113. cookie.setPath("/");
    114. response.addCookie(cookie)
    115. JSONObject jsonObject = new JSONObject();
    116. jsonObject.put("status", true);
    117. jsonObject.put("code",20000);
    118. jsonObject.put("data",refreshToken);
    119. out.write(new ObjectMapper().writeValueAsString(jsonObject));
    120. ;*/
    121. PrintWriter out = response.getWriter();
    122. out.write(new ObjectMapper().writeValueAsString(ResponseData.success(refreshToken)));
    123. out.flush();
    124. out.close();
    125. ResponseData.success(response);
    126. }
    127. }
    128. //登录失败的处理
    129. public class MyFailHandler implements AuthenticationFailureHandler {
    130. @Override
    131. public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
    132. AuthenticationException e) throws IOException, ServletException {
    133. ResponseMsgUtil.sendFailMsg(e.getMessage(), response);
    134. }
    135. }
    136. }

    这里介绍比较重要的一个jwt过滤逻辑,所有的请求都必须携带token,并且跟库里的token比较,未携带或不可用就返回无token或超时,对应vue-element-admin里面的50017的code:

    1. public class JwtAuthenticationFilter extends OncePerRequestFilter {
    2. private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
    3. private JsonWebTokenUtil tokenUtil;
    4. private UserServiceImpl userServiceImpl;
    5. private RequestMatcher requestMatcher;
    6. //private List requestMatcher;
    7. private RefreshTokenMapper refreshTokenMapper;
    8. public JwtAuthenticationFilter(JsonWebTokenUtil tokenUtil, UserServiceImpl userServiceImpl, RequestMatcher requestMatcher,RefreshTokenMapper refreshTokenMapper) {
    9. this.tokenUtil = tokenUtil;
    10. this.userServiceImpl = userServiceImpl;
    11. this.requestMatcher = requestMatcher;
    12. this.refreshTokenMapper = refreshTokenMapper;
    13. }
    14. @Override
    15. protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
    16. logger.info(String.format("请求路径-->%s,是否应该被过滤掉-->%s",request.getRequestURL(),requestMatcher.matches(request)));
    17. return requestMatcher.matches(request);
    18. }
    19. @Override
    20. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    21. throws ServletException, IOException {
    22. //从请求头部获取json web token
    23. String jwt = request.getHeader(tokenUtil.getHeader());
    24. logger.info("urls:"+request.getRequestURI()+"XXXXXX"+request.getRequestURI().substring(0,request.getRequestURL().indexOf("/")));
    25. if (StringUtils.hasLength(jwt) && !jwt.equals("null") && !jwt.equals("undefined")) {
    26. //从jwt中获取用户名,这里应该考虑过期时间,超过过期时间的话获取不到username
    27. //TODO
    28. String username = tokenUtil.getUsernameFromToken(jwt);
    29. if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
    30. LambdaQueryWrapper queryWrapper = new QueryWrapper().lambda().eq(RefreshTokenEntity::getUsename, username);
    31. RefreshTokenEntity refreshTokenEntity = refreshTokenMapper.selectOne(queryWrapper);
    32. String tokenInMysql = StringUtils.isEmpty(refreshTokenEntity) ? "null" : refreshTokenEntity.getToken();
    33. if (jwt.equals(tokenInMysql)) {
    34. logger.info("JwtAuthenticationFilter,username:" + username);
    35. //通过用户名查询
    36. UserDetails userDetails = userServiceImpl.loadUserByUsername(username);
    37. //创建认证信息
    38. UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username,
    39. userDetails.getPassword(), userDetails.getAuthorities());
    40. SecurityContextHolder.getContext().setAuthentication(authentication);
    41. }
    42. }
    43. filterChain.doFilter(request, response);
    44. } else {
    45. //logger.info(requestMatcher.toString());
    46. reject(request, response);
    47. }
    48. }
    49. private static void reject(HttpServletRequest request,HttpServletResponse response) throws IOException{
    50. /* JSONObject jsonObject = new JSONObject();
    51. jsonObject.put("status", true);
    52. jsonObject.put("code",50054);
    53. jsonObject.put("data","Token expired, please log in again.");
    54. PrintWriter out = response.getWriter();
    55. out.write(new ObjectMapper().writeValueAsString(jsonObject));*/
    56. logger.info("filtered url:"+request.getRequestURI());
    57. PrintWriter out = response.getWriter();
    58. out.write(new ObjectMapper().writeValueAsString(ResponseData.fail(ApiError.from(ApiErrorEnum.HAVE_NO_TOKEN))));
    59. out.flush();
    60. }
    61. }

    因login方法在security中做拦截和返回,但vue-element-admin的每次路由都要进行权限的校验,所以需要构造该/getInfo方法,每次返回的是user对应的role;

    1. public ResponseData loginInfo(HttpServletRequest request) {
    2. // 因为所有url都被过滤,所以这里只需要拿到role就行,不需要考虑token是否过期问题
    3. String token = request.getHeader(jwtTokenUtil.getHeader());
    4. LoginInfoVO loginInfoVO = new LoginInfoVO();
    5. if(StringUtils.hasLength(token) && !token.equals("null")) {
    6. String username = jwtTokenUtil.getUsernameIgnoreExpiration(token);
    7. loginInfoVO.setName(username);
    8. LambdaQueryWrapper queryWrapper = new QueryWrapper().lambda().eq(RoleEntity::getName, username);
    9. List roleEntities = roleDao.selectList(queryWrapper);
    10. List roleList = new ArrayList<>();
    11. for (RoleEntity roleEntity : roleEntities) {
    12. roleList.add(roleEntity.getName());
    13. }
    14. loginInfoVO.setRoles(roleList);
    15. return ResponseData.success(loginInfoVO);
    16. }
    17. return ResponseData.fail(ApiError.from(ApiErrorEnum.TOKEN_EXPIRED));
    18. }

    换了一个登录页,如下,登录后返回token,key已修改成JWTHeaderName: 

     

  • 相关阅读:
    Nginx配置微服务避免actuator暴露
    《低代码指南》——维格云锦囊简介
    LabVIEW开放神经网络交互工具包【ONNX】,大幅降低人工智能开发门槛,实现飞速推理
    化繁为简,聊一聊复制状态机系统架构抽象
    一款超强的 Python 分析工具!
    根据语义切分视频
    报告解读下载 | 7月《中国数据库行业分析报告》重磅发布!精彩抢先看!
    Flink动态更新维表
    如何摆脱打工人任人宰割的命运
    小程序内容管理系统设计
  • 原文地址:https://blog.csdn.net/yezonggang/article/details/126055197