springboot2.7+security+jwt 不使用UserDetailsService

描述: 

       网上代码大部分都使用实体类实现 UserDetails,同时实现 UserDetailsService,对于不熟悉 security 的开发人员在开发登录接口时不清楚需要实现这两个接口,对于在配置权限时,需要重构登录接口。

       如果不想实现 UserDetailsService 需要自己实现 AuthenticationProvider 接口

AuthenticationProvider 接口

  • authenticate 方法来验证,就是验证用户身份
  • supports 用来判断当前 AuthenicationProvider 是否对应支持 Authentication

代码部分如下

       由于在 springboot2.7 版本及之后版本使用的 security5.7.1,该版本中之前需要继承的 WebSecurityConfigurerAdapter 类已经过时,所以使用最新配置方式如下

securityConfig

代码语言:javascript
复制
package smart.property.admin.controller.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import smart.property.admin.controller.filter.*;

import java.util.ArrayList;
import java.util.List;

/**

  • BUG不找我

  • @author huatao

  • @date 2023/4/1 15:40
    /
    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {
    @Autowired
    private MyAuthenticationProvider myAuthenticationProvider;
    @Autowired
    private SelfAccessDecisionManager selfAccessDecisionManager;
    @Autowired
    private SelfFilterInvocationSecurityMetadataSource selfFilterInvocationSecurityMetadataSource;
    /
    *

    • 获取AuthenticationManager(认证管理器),登录时认证使用
    • @param authenticationConfiguration
    • @return
    • @throws Exception
      */
      @Bean
      public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
      final List<GlobalAuthenticationConfigurerAdapter> configurers = new ArrayList<>();
      //自定义验证账户密码
      configurers.add(new GlobalAuthenticationConfigurerAdapter() {
      @Override
      public void configure(AuthenticationManagerBuilder auth){
      // auth.doSomething()
      auth.authenticationProvider(myAuthenticationProvider);
      }
      }
      );
      authenticationConfiguration.setGlobalAuthenticationConfigurers(configurers);
      return authenticationConfiguration.getAuthenticationManager();
      }

    /**

    • security 配置
    • @param httpSecurity
    • @return
    • @throws Exception
      */
      @Bean
      public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
      httpSecurity.authorizeRequests()
      .anyRequest().authenticated()
      .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
      @Override
      public <O extends FilterSecurityInterceptor> O postProcess(O o) {
      o.setSecurityMetadataSource(selfFilterInvocationSecurityMetadataSource); //动态获取url权限配置
      o.setAccessDecisionManager(selfAccessDecisionManager); //权限判断
      return o;
      }
      })
      .and()
      .formLogin() //关闭form提交方式
      .disable()
      .sessionManagement()
      .sessionCreationPolicy(SessionCreationPolicy.STATELESS)//禁用session
      .and()
      .httpBasic()
      .authenticationEntryPoint(new UnLoginUserEntryPoint())
      // .userDetailsService(userDetailsService)
      // .cors().disable()
      // .httpBasic()
      .and()
      .csrf().disable()
      // .cors().disable()
      ;
      return httpSecurity.build();
      }

    /**

    • 释放路径
    • @return
      */
      @Bean
      public WebSecurityCustomizer webSecurityCustomizer() {
      return new WebSecurityCustomizer() {
      @Override
      public void customize(WebSecurity web) {
      web.ignoring().antMatchers("/login/**");// 放行login开头路径
      }
      };
      }

    /**

    • 加密方式注入容器
    • @return
      */
      @Bean
      public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
      }

}

自定义 AuthenticationProvider

代码语言:javascript
复制
package smart.property.admin.controller.filter;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

/**

  • BUG不找我

  • 重定义账户密码验证过滤器

  • @author huatao

  • @date 2023/4/6 14:43
    */
    @Component
    public class MyAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    //由于我们在service已经验证过账户密码等操作,这里直接组装返回数据即可
    return new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), null, authentication.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
    return authentication.equals(
    UsernamePasswordAuthenticationToken.class);
    }

}

FilterInvocationSecurityMetadataSource 实现类

代码语言:javascript
复制
package smart.property.admin.controller.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import smart.property.admin.bean.menu.MenuInfoPO;
import smart.property.admin.bean.role.RoleInfoPO;
import smart.property.admin.service.menu.MenuInfoService;

import java.util.Collection;
import java.util.List;

/**

  • BUG不找我

  • 获取当前路径所需要的权限

  • @author huatao

  • @date 2022/7/18 12:00
    */
    @Slf4j
    @Component
    public class SelfFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    private MenuInfoService menuInfoService;
    @Autowired
    public SelfFilterInvocationSecurityMetadataSource(MenuInfoService menuInfoService) {
    this.menuInfoService = menuInfoService;
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
    //获取请求路径
    String requestUrl = ((FilterInvocation) o).getRequestUrl();
    log.info("当前请求路径:{}",requestUrl);
    //获取所有路径以及对应角色
    List<MenuInfoPO> urlList = menuInfoService.getUrlList();
    //声明保存可访问角色集合
    String[] str = null;
    //获取每个路径所需要的角色
    for(MenuInfoPO menuUrlVO:urlList){
    //判断请求路径和配置路径是否一致,一致则获取该路径所需要的角色
    if(antPathMatcher.match(menuUrlVO.getMenuUri(),requestUrl)){
    List<RoleInfoPO> roleInfoPOList = menuUrlVO.getRoleInfoPOList();
    //判断路径可以访问的角色是否为null
    str=new String[roleInfoPOList.size()];
    for (int i=0; i<roleInfoPOList.size(); i++){
    str[i] = roleInfoPOList.get(i).getRoleName();

                 }
         }
     };
     //判断是否为空,如果为空直接抛出无权限异常,否则会进入接口
     if(null==str||str.length&lt;1){
         throw new AccessDeniedException(&#34;权限不足,请联系管理员!&#34;);
     }
     return SecurityConfig.createList(str);
    

    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
    return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
    return FilterInvocation.class.isAssignableFrom(aClass);
    }
    }

AccessDecisionManager

代码语言:javascript
复制
package smart.property.admin.controller.filter;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;
import smart.property.admin.bean.login.LoginUserVO;
import smart.property.admin.bean.role.RoleNameVO;
import smart.property.admin.bean.sys.RedisPrefix;
import smart.property.admin.service.role.RoleInfoService;
import smart.property.utils.token.TokenManager;
import smart.property.utils.json.JacksonUtil;

import java.util.Collection;
import java.util.List;

/**

  • BUG不找我

  • 判断当前用户是否具有权限

  • @author huatao

  • @date 2022/7/18 12:03
    */
    @Slf4j
    @Component
    public class SelfAccessDecisionManager implements AccessDecisionManager {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RoleInfoService roleInfoService;
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
    //从请求头获取token
    String token = ((FilterInvocation) o).getHttpRequest().getHeader("token");
    if(HttpMethod.OPTIONS.toString().equals(((FilterInvocation) o).getHttpRequest().getMethod())){
    return;
    }
    // 通过token获取加密信息
    String subjectByToken = TokenManager.getSubjectByToken(token);
    //将json转为登录用户
    LoginUserVO loginUserVO = JacksonUtil.toBean(subjectByToken, LoginUserVO.class);
    //通过用户名获取用户信息
    String roleInfo = stringRedisTemplate.opsForValue().get(RedisPrefix.USER_ROLE + loginUserVO.getUsername());
    List<RoleNameVO> roleNameVOList =null;
    //判断如果redis不存在用户角色则从数据库查询,如果存在则直接转为角色集合
    if(StringUtils.isBlank(roleInfo)){
    roleNameVOList = roleInfoService.getRoleByUserId(loginUserVO.getId());
    stringRedisTemplate.opsForValue().set(RedisPrefix.USER_ROLE + loginUserVO.getUsername(),JacksonUtil.toString(roleNameVOList));
    }else{
    roleNameVOList = JacksonUtil.jsonToBeanList(roleInfo, RoleNameVO.class);
    }
    for (ConfigAttribute configAttribute : collection) {
    String needRole = configAttribute.getAttribute();
    for (int i=0;i<roleNameVOList.size();i++ ) {
    if (roleNameVOList.get(i).getRoleName().equals(needRole)){
    return;
    }
    }
    }
    //无访问权限
    throw new AccessDeniedException("无访问权限");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
    return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
    return true;
    }
    }

AuthenticationEntryPoint

代码语言:javascript
复制
package smart.property.admin.controller.filter;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import smart.property.admin.bean.sys.R;
import smart.property.admin.bean.sys.RespEnum;
import smart.property.utils.json.JacksonUtil;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**

  • BUG不找我

  • @author huatao

  • @date 2023/4/6 14:27
    /
    public class UnLoginUserEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    response.setCharacterEncoding("UTF-8");
    R error = R.error(RespEnum.ERROR_NO_ROLE.getCode(), RespEnum.ERROR_NO_ROLE.getMessage());
    PrintWriter writer = response.getWriter();
    writer.print(JacksonUtil.toString(error));
    writer.flush();
    writer.close();
    }
    }

登录处理接口

代码语言:javascript
复制
/*

  • BUG不找我

  • 登录业务

  • @author huatao

  • @date 2023/4/1 9:30
    */
    @RestController
    @RequestMapping("login")
    public class LoginController {
    private final StringRedisTemplate stringRedisTemplate;
    private final LoginService loginService;

    @Autowired
    public LoginController(StringRedisTemplate stringRedisTemplate,LoginService loginService) {
    this.stringRedisTemplate = stringRedisTemplate;
    this.loginService = loginService;
    }

    /**

    • 获取验证码

    • @return
      */
      @GetMapping("getCode")
      public R<CodeVO> getCode(){

      return R.success(loginService.getCode());
      }

    /**

    • 登录
    • @param loginDTO
    • @return
      */
      @PostMapping("doLogin")
      public R<LoginUserVO> doLogin(@Valid @RequestBody LoginDTO loginDTO, HttpServletResponse response){
      LoginUserVO loginUserVO = loginService.doLogin(loginDTO);
      //创建token
      response.setHeader("token", TokenManager.createToken(JacksonUtil.toString(loginUserVO)));
      //创建刷新token
      response.setHeader("refreshToken",TokenManager.createRefreshToken(JacksonUtil.toString(loginUserVO)));
      return R.success(loginUserVO);
      }
      }
  • 代码语言:javascript
    复制
    package smart.property.admin.service.login;
    
    
    
    

    import cn.hutool.captcha.CaptchaUtil;
    import cn.hutool.captcha.ShearCaptcha;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.stereotype.Service;
    import smart.property.admin.bean.login.CodeVO;
    import smart.property.admin.bean.login.LoginDTO;
    import smart.property.admin.bean.login.LoginUserVO;
    import smart.property.admin.bean.role.RoleInfoPO;
    import smart.property.admin.bean.role.RoleNameVO;
    import smart.property.admin.bean.sys.RedisPrefix;
    import smart.property.admin.bean.sys.SysException;
    import smart.property.admin.bean.user.UserInfoPO;
    import smart.property.admin.mapper.user.UserMapper;
    import smart.property.admin.service.role.RoleInfoService;
    import smart.property.utils.convert.ConvertUtils;
    import smart.property.utils.json.JacksonUtil;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.UUID;
    import java.util.concurrent.TimeUnit;

    /**

    • BUG不找我

    • 自定义登录

    • @author huatao

    • @date 2023/4/1 16:05
      */
      @Service
      public class LoginServiceImpl implements LoginService{
      private final UserMapper userMapper;
      private final PasswordEncoder passwordEncoder;
      private final StringRedisTemplate stringRedisTemplate;
      private final AuthenticationManager authenticationManager;
      private final RoleInfoService roleInfoService;

      @Autowired
      public LoginServiceImpl(UserMapper userMapper, PasswordEncoder passwordEncoder, StringRedisTemplate stringRedisTemplate, AuthenticationManager authenticationManager,RoleInfoService roleInfoService) {
      this.userMapper = userMapper;
      this.passwordEncoder = passwordEncoder;
      this.stringRedisTemplate = stringRedisTemplate;
      this.authenticationManager = authenticationManager;
      this.roleInfoService = roleInfoService;
      }

      @Override
      public LoginUserVO doLogin(LoginDTO loginDTO) {
      UserInfoPO userByUsername = userMapper.getUserByUsername(loginDTO.getUsername());

       //获取用户拥有的角色
       List&lt;RoleNameVO&gt; roleByUserId = roleInfoService.getRoleByUserId(userByUsername.getId());
       stringRedisTemplate.opsForValue().set(RedisPrefix.USER_ROLE+userByUsername.getUsername(), JacksonUtil.toString(roleByUserId),2,TimeUnit.HOURS);
       //3使用ProviderManager auth方法进行验证
       UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userByUsername.getUsername(), userByUsername.getPassword());
       authenticationManager.authenticate(usernamePasswordAuthenticationToken);
       return ConvertUtils.convert(userByUsername, LoginUserVO.class);
      

      }

    }

    判断是否有token

    代码语言:javascript
    复制
    package smart.property.admin.controller.filter;

    import org.apache.commons.lang3.StringUtils;
    import org.springframework.core.annotation.Order;
    import org.springframework.http.HttpMethod;
    import org.springframework.stereotype.Component;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.web.servlet.HandlerInterceptor;
    import smart.property.admin.bean.sys.R;
    import smart.property.admin.bean.sys.RespEnum;
    import smart.property.admin.bean.sys.SysException;
    import smart.property.utils.json.JacksonUtil;
    import smart.property.utils.token.TokenManager;

    import javax.servlet.*;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;

    /**

    • BUG不找我

    • 判断请求是否包含请求头

    • @author huatao

    • @date 2023/4/13 16:22
      */
      @Component
      @Order(-100000)
      public class TokenFilter implements Filter {
      private final AntPathMatcher antPathMatcher = new AntPathMatcher();

      @Override
      public void init(FilterConfig filterConfig) throws ServletException {

      }

      @Override
      public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
      HttpServletRequest request = (HttpServletRequest) servletRequest;
      if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
      filterChain.doFilter(servletRequest, servletResponse);
      } else if (antPathMatcher.match("/admin/login/**", request.getRequestURI())) {
      filterChain.doFilter(servletRequest, servletResponse);
      } else {
      String token = request.getHeader("token");
      if (StringUtils.isBlank(token)) {
      servletResponse.setCharacterEncoding("UTF-8");
      PrintWriter writer = servletResponse.getWriter();
      writer.print(JacksonUtil.toString(R.error(RespEnum.ERROR_NO_LOGIN.getCode(), RespEnum.ERROR_NO_LOGIN.getMessage())));
      writer.flush();
      writer.close();
      } else {
      filterChain.doFilter(servletRequest, servletResponse);
      }
      }

      }

      @Override
      public void destroy() {

      }
      }

     跨域处理

    代码语言:javascript
    复制
    package smart.property.admin.controller.config;

    import org.springframework.boot.web.servlet.FilterRegistrationBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.Ordered;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
    import org.springframework.web.filter.CorsFilter;
    /**

    • BUG不找我
    • 处理跨域
    • @author huatao
    • @date 2023/4/1 19:49
      */

    @Configuration
    public class CorsConfig {

    @Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin(&#34;http://127.0.0.1:8080&#34;); // 1允许任何域名使用
        corsConfiguration.addAllowedOrigin(&#34;http://localhost:8080&#34;); // 1允许任何域名使用
        corsConfiguration.addAllowedHeader(&#34;*&#34;); // 2允许任何头
        corsConfiguration.addAllowedMethod(&#34;*&#34;); // 3允许任何方法(post、get等)
        source.registerCorsConfiguration(&#34;/**&#34;, corsConfiguration);
        //如果没有下面两行则会出现无权限访问时会出现跨域错误
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new CorsFilter(source));
        // 代表这个过滤器在众多过滤器中级别最高,也就是过滤的时候最先执行
        filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return filterRegistrationBean;
    }
    

    }

    不对的地方欢迎指出!~

    转载请标明地址谢谢!!!