自定义SpringSecurity

224 阅读3分钟

SAAA系统,权限要求时间限制,有超级管理员权限,单点登录,权限缓存到redis,密码加密,权限变更踢出登录

添加依赖包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

配置类 注意55行的.access()可以指定逻辑来处理 而非之前一个一个接口配置(如果配置规则有两个满足http路径,先配置的生效 不会之后匹配的规则,所以/user/{id}配置/user/* 一定要在/user/xxx之后)


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter  {


    private final UserDetailsService userDetailsService;
    public final SecurityComponent securityComponent;
    public final PasswordEncoder passwordEncoder;
    private final RedisSecurityContextRepository redisSecurityContextRepository;
    private final UserDetailsPasswordService userDetailsPasswordService;
    private final UserDetailsChecker userDetailsChecker;
    
    @Bean
    @Primary
    public RedisSecurityContextRepository redisSecurityContextRepository(){
       return this.redisSecurityContextRepository;
    }



    public SecurityConfig(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder,
                     UserDetailsPasswordService userDetailsPasswordService,UserDetailsChecker userDetailsChecker,
                     ObjectMapper objectMapper, RedisTemplate<String, Object> securityRedisTemplate) {
       this.userDetailsService = userDetailsService;
       this.userDetailsChecker=userDetailsChecker;
       this.userDetailsPasswordService=userDetailsPasswordService;
       this.passwordEncoder=passwordEncoder;
       this.redisSecurityContextRepository=new RedisSecurityContextRepository(securityRedisTemplate,
             RedisKeyEnum.LBSS_TOKEN.getKey(),
             "lbss-token", true, Duration.ofHours(2));
       this.securityComponent = new SecurityComponent(objectMapper,redisSecurityContextRepository);


    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
       http.csrf().disable();
       http.addFilterBefore(new PreLoginFilter(), UsernamePasswordAuthenticationFilter.class);
       http.formLogin()
             .authenticationDetailsSource(CustomWebAuthenticationDetails::new)
             .failureHandler(securityComponent)
             .successHandler(securityComponent);
       http.exceptionHandling().accessDeniedHandler(securityComponent)
             .authenticationEntryPoint(securityComponent);
       http.sessionManagement().disable();
       http.securityContext()
             .securityContextRepository(securityComponent.redisSecurityContextRepository)
             .and().logout()
             .addLogoutHandler(securityComponent);
       http.authorizeRequests()
             .antMatchers("/login"....).permitAll()
             .antMatchers("/employee/getUserInfo"....).authenticated()
             .anyRequest()
             .access("@httpGlobalSecurityProcess.hasPermission(request,authentication)")
       ;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
       daoAuthenticationProvider.setPreAuthenticationChecks(userDetailsChecker);
       daoAuthenticationProvider.setUserDetailsService(userDetailsService);
       daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
       daoAuthenticationProvider.setUserDetailsPasswordService(userDetailsPasswordService);
       auth.authenticationProvider(daoAuthenticationProvider).eraseCredentials(true);
    }
    @Value("${lbss.dev:false}")
    boolean isDev;

    @Override
    public void configure(WebSecurity webSecurity) throws Exception {
       List<String> antPatterns =new ArrayList<>();
       Collections.addAll(antPatterns,"/actuator/health".....);
       if (isDev) {
          antPatterns.add("/auth/**");
       }
       webSecurity.ignoring().antMatchers(antPatterns.toArray(new String[0]));
    }

}

具体的匹配规则和全权限处理(要不然得一个一个接口都配置)
12-15行:将当前请求统一格式,从已认证用户权限中匹配,如果没找到则走正则匹配(这里主要是路径传参 /user/1952->/user/{id})

@Component("httpGlobalSecurityProcess")
@Log4j2
public class HttpGlobalSecurityProcess {
    @Autowired
    private UserDetailsChecker userDetailsChecker;

    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        Object principal = authentication.getPrincipal();
        if (principal instanceof Employee) {
            Employee user = ((Employee) principal);
            userDetailsChecker.check(user);
            final String http = MyGrantedAuthority.authority(request.getRequestURI(),request.getMethod());
            final MyGrantedAuthority myGrantedAuthority = user.getAuthorityMap().get(http);
            return myGrantedAuthority==null?//如果没有找到匹配的则直接遍历所有的元素去匹配
                    user.getAuthorityMap().values().stream().anyMatch(x->x.match(http)):myGrantedAuthority.match(http);
            }
        return false;
    }
    public final static Collection<GrantedAuthority> AllAuth;

    public final static Collection<Auth> AllAuthTree;
    static {
        AllAuthTree= new ArrayList<>();
        AllAuthTree.add(new Auth(){{setRoute("root");setName("全权限");}});
        AllAuth= new HashSet<>();
        AllAuth.add(new SimpleGrantedAuthority(".*"));
    }
}

用户状态校验和异常类

@Component
public class MyUserDetailsChecker implements UserDetailsChecker {
    
    @Override
    public void check(UserDetails user) {
        Employee employee=((Employee)user);
        if (ClientAppStatus.ENABLE!=employee.getClientAppSystem().getClientApp().getStatus())
            throw new SysStopException("系统已"+employee.getClientAppSystem().getClientApp().getStatus().getMean());
        if (ClientAppModuleStatus.ENABLE!=employee.getClientAppSystem().getStatus())
            throw new SysStopException("系统模块已"+employee.getClientAppSystem().getStatus().getMean());
        if (!LocalDateTime.now().isBefore(employee.getClientAppSystem().getClientApp().getEndTime()))
            throw new SysExpiredException("系统已过期");
        if (!user.isAccountNonLocked())
            throw new LockedException("用户已被锁定");
        if (!user.isEnabled())
            throw new DisabledException("用户未启用");
        if (!user.isAccountNonExpired())
            throw new AccountExpiredException("用户已过期");
        if (!user.isCredentialsNonExpired())
            throw new CredentialsExpiredException("用户凭据已过期");

    }
}

public class AuthStopException extends AccessDeniedException {

    public AuthStopException(String msg) {
        super(msg);
    }
}
public class SysStopException extends  AccountStatusException {

    public SysStopException(String msg) {
        super(msg);
    }
}

自定义权限,(spring使用的是SimpleGrantedAuthority权限即是路径)
中有过期时间和当前权限的路径(为了方便以已经拼为如"/user/get[GET]")

@Data
@NoArgsConstructor
public class MyGrantedAuthority implements GrantedAuthority {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime expiredTime= DefaultValueUtils.expiredDateTime;
    private ClientAppModuleStatus status;
    private String authority;

    public MyGrantedAuthority(ClientAppModule clientAppModule,  String url) {
        this.authority=url;
        this.status=clientAppModule==null?ClientAppModuleStatus.STOP:clientAppModule.getStatus();
        this.expiredTime=clientAppModule==null?DefaultValueUtils.expiredDateTime:clientAppModule.getExpiredTime();
    }
    public MyGrantedAuthority(Auth auth,  String url) {
        this.authority=url;
        this.status=auth.getStatus();
        this.expiredTime=auth.getExpiredTime();
    }


    public boolean match(String httpUri) {
        if (httpUri.equals(authority) || httpUri.matches(regex())){
            if (!ClientAppModuleStatus.ENABLE.equals(status))
                throw new AuthStopException("权限已停用");
            if (!LocalDateTime.now().isBefore(expiredTime))
                throw new AuthExpiredException("权限已过期");
            return true;
        }
        return false;
    }

    private String regex(){
        return authority.replace("[", "\[").replace("]", "\]");
    }
    public static String authority(String uri,String method){
        return uri + "[" + method + "]";
    }

}

用户信息类(登录后缓存到redis中)
authorityMap存的就是所有的权限(可访问的请求),authTree存的是可以访问的菜单
一级路由——二级路由...路由——按钮(权限)——接口请求 (均为1对多)

@EqualsAndHashCode(callSuper = true)
@Data@Accessors(chain = true)
public class Employee extends EntityBaseByTime implements UserDetails {
    protected Long id;
    private String companyId;
    private String phone;
    private String password;
    private String trueName;
    private AgrEmployeeStatus status;
    private String clientId;

    // 员工或者管理员
    private EmployeeType type;
    
    //员工有效期
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startTime;
    
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endTime;

    //存入可以访问的请求 {带有通配符合*M,无通配符*N}
    // 先setO(1)判断在不在 不在则循环去通配 平均复杂度O(1)+O(M/2) 最好O(1) 最坏O(1)+O(N+M)——此时为恶意接口访问
    private Map<String, MyGrantedAuthority> authorityMap;
    private List<Long> roleIds;
    private Collection<EmployeeRole> roles;
    private Collection<Auth> authTree=Collections.EMPTY_LIST;


    private String parkingAreaName;
    private Long parkingAreaId;
    private String bigScreenVideo;
    private ClientAppConfig clientAppConfig;
    private ClientAppSystem clientAppSystem;
    private String userAgent;



    @Override
    @JsonIgnore
    public Collection<GrantedAuthority> getAuthorities() {
       return Collections.emptyList();
    }

    @Override
    public String getUsername() {
       return companyId+":"+phone;
    }

    @Override
    public boolean isAccountNonExpired() {
       final LocalDateTime now = LocalDateTime.now();
       return (startTime == null || now.isAfter(startTime))&&(endTime == null || now.isBefore(endTime));
    }
    @Override
    public boolean isAccountNonLocked() {
       return !AgrEmployeeStatus.OFF.equals(status);
    }
    @Override
    public boolean isCredentialsNonExpired() {
       return true;
    }
    @Override
    public boolean isEnabled() {
       return AgrEmployeeStatus.ON.equals(status);
    }

    public ClientApp getClientApp() {
       return clientAppSystem==null?null:clientAppSystem.getClientApp();
    }
}
@Data
public class CustomWebAuthenticationDetails implements Serializable {
    private final String userAgentInfo;
    private final String ip;
    private final String sessionId;

    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        this.userAgentInfo = request.getHeader("user-agent");
        this.ip = IpUtils.getIpAddr(request);
        HttpSession session = request.getSession(false);
        this.sessionId = (session != null) ? session.getId() : null;
    }

    public CustomWebAuthenticationDetails() {
        this.ip = this.sessionId = this.userAgentInfo = null;
    }

    public CustomWebAuthenticationDetails(final String ip, final String sessionId, String userAgentInfo) {
        this.ip = ip;
        this.sessionId = sessionId;
        this.userAgentInfo = userAgentInfo;
    }


    public String getUserAgentInfo() {
        return userAgentInfo;
    }

    public String getIp() {
        return ip;
    }

    public String getSessionId() {
        return sessionId;
    }

    public String detail() {
        final UserAgentInfo userAgentInfo = UserAgentUtil.parse(getUserAgentInfo());
        if (userAgentInfo.hasOsInfo()) {
            return String.format("登录ip:%s,登录平台:%s,浏览器:%s", getIp(), userAgentInfo.getOsFamily(), userAgentInfo.getUaFamily());
        }
        return String.format("登录ip:%s,登录信息:%s", getIp(), getUserAgentInfo());
    }
}

认证事件处理,设置请求头和返回信息

public class SecurityComponent implements AuthenticationFailureHandler, AuthenticationSuccessHandler,
        AccessDeniedHandler, AuthenticationEntryPoint, LogoutHandler, LogoutSuccessHandler  {

    private final ObjectMapper objectMapper;
    private static final Log logger = LogFactory.getLog(SecurityComponent.class);
    private final String contentType = MediaType.APPLICATION_JSON_UTF8_VALUE;
    public final RedisSecurityContextRepository redisSecurityContextRepository;

    public SecurityComponent(ObjectMapper objectMapper,
                             RedisSecurityContextRepository redisSecurityContextRepository) {
        this.objectMapper = objectMapper;
        this.redisSecurityContextRepository =redisSecurityContextRepository;

    }

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(contentType);
        response.getWriter().write(objectMapper.writeValueAsString(
                SecureResponseEnum.AUTH_FAIL.createSecureModel("验证异常:" + authException.getMessage())));
//     response.flushBuffer();
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {
        logger.info("access denied", exception);
        response.setContentType(contentType);
        if (exception instanceof AccountStatusException){
            response.setStatus(HttpStatus.FORBIDDEN.value());
            response.getWriter().write(
                    objectMapper.writeValueAsString(SecureResponseEnum.ACCESS_DENIED.createSecureModel(exception.getMessage())));
        }else {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.getWriter()
                    .write(objectMapper.writeValueAsString(SecureResponseEnum.LOGIN_FAIL.createSecureModel(
                            HttpStatus.UNAUTHORIZED.value(), "请检查用户名和密码")));
        }

//     response.flushBuffer();
    }



    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        final Employee employee = (Employee) authentication.getPrincipal();employee.setPassword(null);
        logger.error("login success!".concat(employee.getUsername()));
        response.setContentType(contentType);
        response.getWriter().write(objectMapper.writeValueAsString( SecureResponseEnum.LOGIN_SUCCESS.createSecureModel(
                        0, "登录成功")));
//     response.flushBuffer();
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDenied) throws IOException, ServletException {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.setContentType(contentType);
        logger.info("access denied", accessDenied);
        if (accessDenied instanceof InvalidCsrfTokenException) {
            response.getWriter().write(objectMapper
                    .writeValueAsString(SecureResponseEnum.ACCESS_DENIED.createSecureModel("错误的csrfToken")));
        } else if (accessDenied instanceof MissingCsrfTokenException) {
            response.getWriter().write(
                    objectMapper.writeValueAsString(SecureResponseEnum.ACCESS_DENIED.createSecureModel("csrfToken缺失")));
        } else if (accessDenied instanceof AuthorizationServiceException) {
            response.getWriter().write(
                    objectMapper.writeValueAsString(SecureResponseEnum.ACCESS_DENIED.createSecureModel("权限验证异常")));
        }else {
            response.getWriter().write(
                    objectMapper.writeValueAsString(SecureResponseEnum.ACCESS_DENIED.createSecureModel(accessDenied.getMessage())));
        }
//     response.flushBuffer();
    }
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        redisSecurityContextRepository.clearContext(request, SecurityContextHolder.getContext());
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        if (logger.isDebugEnabled())
            logger.debug("登出成功!!!");
        SecureResultModel resultModel = SecureResponseEnum.LOGOUT_SUCCESS.createSecureModel();
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(contentType);
        response.getWriter().write(objectMapper.writeValueAsString(resultModel));
    }
}

自定义SecurityContextRepository (默认是会话 可以配置为Spring的Redis)

  • loadContext 加载请求上下文
  • saveContext ,singleUserCheckSave处理单点登录,如果是单点登录则删除所有的token,并把当前的token加上 (其实可以直接setvalue) redis中登录一个用户有两个对象,token-》用户信息,用户名-》token

public class RedisSecurityContextRepository implements SecurityContextRepository {
    static final String usernameRedisModule="username";
    static final String tokenRedisModule="token";

    public final RedisTemplate<String, Object> securityRedisTemplate;

    public final String redisKeyTokenKey;
    public final String redisKeyUsernameKey;
    private final String headerTokenKey;
    private final String refreshKey;
    private final boolean enableSso;
    private final Duration timeout;

    public RedisSecurityContextRepository(RedisTemplate<String, Object> securityRedisTemplate,
                                 String redisKey,String headerTokenKey,
                                 boolean enableSso,Duration timeout) {
       this.securityRedisTemplate = securityRedisTemplate;
       this.redisKeyUsernameKey=redisKey.concat(":").concat(usernameRedisModule);
       this.redisKeyTokenKey=redisKey.concat(":").concat(tokenRedisModule);
       this.refreshKey=redisKey.concat("refresh_token");
       this.headerTokenKey=headerTokenKey;
       this.enableSso=enableSso;
       this.timeout=timeout;

    }

    private String redisKeyUsernameKey(String username) {
       return String.format("%s:%s", redisKeyUsernameKey, username);
    }
    private String redisKeyTokenKey(String token) {
       return String.format("%s:%s", redisKeyTokenKey, token);
    }
    public String getTokenByUsername(String username){
       return (String) securityRedisTemplate.opsForValue().get(redisKeyUsernameKey(username));
    }
    public Authentication getAuthenticationByToken(String token){
       return (Authentication) securityRedisTemplate.opsForValue().get(redisKeyTokenKey(token));
    }
    public void updateAuthenticationByToken(String token ,Authentication authentication){
       securityRedisTemplate.opsForValue().set(redisKeyTokenKey(token), authentication, timeout);
    }
    public void removeAuthentication(String redisRepository,String username) {
       final String usernames = String.format("%s:%s:%s", redisRepository,usernameRedisModule,username);
       final List<String> tokens = securityRedisTemplate.opsForZSet().range(usernames,0,Long.MAX_VALUE)
             .stream().filter(Objects::nonNull)
             .map(token -> String.format("%s:%s:%s", redisRepository, tokenRedisModule, token)).collect(Collectors.toList());
       tokens.add(username);
       securityRedisTemplate.delete(tokens);
    }

    public void removeAuthenticationByUsername(Collection<String> usernames){
       final List<String> redisUsernameKeys = usernames.stream().map(this::redisKeyUsernameKey).collect(Collectors.toList());
       final List<String> redisUsernameTokens=redisUsernameKeys.stream().map(x->securityRedisTemplate.opsForZSet().range(x,0,Long.MAX_VALUE))
             .filter(Objects::nonNull).flatMap(Collection::stream).map(x->redisKeyTokenKey((String)x)).collect(Collectors.toList());
       securityRedisTemplate.delete(redisUsernameKeys);
       securityRedisTemplate.delete(redisUsernameTokens);
    }

    public Set<String> getAllUsername(){
       return securityRedisTemplate.keys(redisKeyUsernameKey("*"));
    }
    public void deleteUsername(String username){
       final String usernameKey = redisKeyUsernameKey(username);
       final String token = (String) securityRedisTemplate.opsForValue().get(usernameKey);
       securityRedisTemplate.delete(List.of(usernameKey,redisKeyTokenKey(token)));
    }

    @Override
    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
       SecurityContext securityContext = SecurityContextHolder.getContext();
       Authentication authentication = securityContext.getAuthentication();
       if (authentication == null) {
          HttpServletRequest request = requestResponseHolder.getRequest();
          String token = request.getHeader(headerTokenKey);
          if (token != null) {
             authentication = (Authentication) securityRedisTemplate.opsForValue().get(redisKeyTokenKey(token));
             securityContext.setAuthentication(authentication);
          }
       }
       requestResponseHolder.getRequest().setAttribute("tokenChecked", authentication);
       return securityContext;
    }

    @Override
    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
       Authentication authentication = context.getAuthentication();
       if (authentication == null || authentication.getPrincipal() == null
             || authentication.getPrincipal().equals("anonymousUser"))
          return;
       if (!isContextSaved(request, context)) {//是否已经认证过(保存过token)了
          String token = UUID.randomUUID().toString().replace("-", "");
          UserDetails userDetails = (UserDetails) authentication.getPrincipal();
          singleUserCheckSave(userDetails.getUsername(), token);
          response.setHeader(headerTokenKey, token);
          ValueOperations<String, Object> operations = securityRedisTemplate.opsForValue();
          operations.set(redisKeyTokenKey(token), authentication, timeout);
          request.setAttribute("tokenChecked", authentication);
       } else {
          Boolean needRefresh = (Boolean) request.getAttribute(refreshKey);
          if (needRefresh != null && needRefresh) {
             String token = request.getHeader(headerTokenKey);
             ValueOperations<String, Object> operations = securityRedisTemplate.opsForValue();
             operations.set(redisKeyTokenKey(token), authentication, timeout);
          }
       }
       if (response.isCommitted())
          request.removeAttribute("tokenChecked");
    }

    private void singleUserCheckSave(String username, String token) {
       String redisKeyUsernameKey = redisKeyUsernameKey(username);
       final ZSetOperations<String, Object> zSetOperations = securityRedisTemplate.opsForZSet();
       if (enableSso){
          final List<String> removeTokens = zSetOperations.range(redisKeyUsernameKey, 0, Long.MAX_VALUE).stream().map(x -> (redisKeyTokenKey((String) x))).collect(Collectors.toList());
          removeTokens.add(redisKeyUsernameKey);
          securityRedisTemplate.delete(removeTokens);
       }else {//这里可以不用要 但是那样会导致在(enableSso==false)条件下zSet集合越来越大
          zSetOperations.removeRangeByScore(redisKeyUsernameKey, 0, Instant.now().getEpochSecond()-24*60*60);
       }
       zSetOperations.add(redisKeyUsernameKey,token, Instant.now().getEpochSecond());
       securityRedisTemplate.expire(redisKeyUsernameKey,timeout);
    }

    @Override
    public boolean containsContext(HttpServletRequest request) {
       String token = request.getHeader(headerTokenKey);
       if (token != null)
          return securityRedisTemplate.hasKey(redisKeyTokenKey(token));
       return false;
    }

    private boolean isContextSaved(HttpServletRequest request, SecurityContext context) {
       Authentication authentication = context.getAuthentication();
       if (request.getAttribute("tokenChecked") != authentication)
          return false;
       else {
          String token = request.getHeader(headerTokenKey);
          securityRedisTemplate.expire(redisKeyTokenKey(token), timeout.getSeconds(), TimeUnit.SECONDS);
          UserDetails userDetails = (UserDetails) authentication.getPrincipal();
          securityRedisTemplate.opsForZSet().add(
                redisKeyUsernameKey(userDetails.getUsername()),token, Instant.now().getEpochSecond());
          securityRedisTemplate.expire(redisKeyUsernameKey(userDetails.getUsername()), timeout.getSeconds(),
                TimeUnit.SECONDS);

          return true;
       }
    }

    public void clearContext(HttpServletRequest request, SecurityContext context) {
       Authentication authentication = context.getAuthentication();
       if (request.getAttribute("tokenChecked") == authentication) {
          UserDetails userDetails = (UserDetails) authentication.getPrincipal();
          securityRedisTemplate.delete(redisKeyUsernameKey(userDetails.getUsername()));
       }
       String token = request.getHeader(headerTokenKey);
       if (token != null)
          securityRedisTemplate.delete(redisKeyTokenKey(token));
       context.setAuthentication(null);
    }


}

自定义的密码加解密类 (忽略,可以使用Spring的PasswordEncoderFactories) 这里是因为之前用的MD5换成BCrypt upgradeEncoding是说是否更新密码格式

@Configuration
public class BeanRegister {

    @Value("${rsa.encrypt.privateKey}")
    private String privateKey;
    @Value("${rsa.encrypt.publicKey}")
    private String publicKey;

    static class OldPasswordEncoder implements PasswordEncoder{
        private final PasswordEncoder passwordEncoder=new BCryptPasswordEncoder(10);

        @Override
        public String encode(CharSequence rawPassword) {
            throw new HaveReasonException("不能加密");
        }

        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            String rawPwd = DigestUtils.md5Hex(String.format("%s$$%s", PreLoginFilter.LoginParameter.get().getPhone(), DigestUtils.md5Hex(String.valueOf(rawPassword))));
            return passwordEncoder.matches(rawPwd,encodedPassword);
        }

        @Override
        public boolean upgradeEncoding(String encodedPassword) {
            return false;
        }
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        String encodingId = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put(encodingId, new BCryptPasswordEncoder(10));
        final DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(encodingId, encoders);
        passwordEncoder.setDefaultPasswordEncoderForMatches(new OldPasswordEncoder());
        return new RSAPasswordEncoder(passwordEncoder,privateKey,publicKey);
    }



}

public class RSAPasswordEncoder implements PasswordEncoder{

    private  PasswordEncoder passwordEncoder;
    private final RSAPrivateKeyPair rsaPrivateKeyPair;

    private static String privateKey;
    private static String publicKey;

    public RSAPasswordEncoder(PasswordEncoder passwordEncoder, String privateKey, String publicKey) {
        this(privateKey,publicKey);
        this.passwordEncoder=passwordEncoder;
    }

    public RSAPasswordEncoder(String privateKey, String publicKey) {
        this.rsaPrivateKeyPair=new RSAPrivateKeyPair(privateKey);
        RSAPasswordEncoder.privateKey = privateKey;
        RSAPasswordEncoder.publicKey = publicKey;
    }

    @Override
    public boolean upgradeEncoding(String encodedPassword) {
        return false;
//        return  !encodedPassword.contains("{");
    }

    @Override
    public String encode(CharSequence rawPassword){
        return passwordEncoder.encode(rawPassword.length()>30?decrypt(rawPassword):rawPassword);
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
//        return passwordEncoder.matches((rawPassword), encodedPassword);
        return passwordEncoder.matches(rawPassword.length()>30?decrypt(rawPassword):rawPassword, encodedPassword);
    }
    private String decrypt(CharSequence rawPassword){
        return RsaUtils.decrypt(rsaPrivateKeyPair, (String)rawPassword);
    }

    public static String getPrivateKey() {
        return privateKey;
    }

    public static String getPublicKey() {
        return publicKey;
    }
}

用户类 UserDetailsService,UserDetailsPasswordService 在loadUserByUsername查到用户信息 并计算权限


@Service
@Primary
public class EmployeeService implements UserDetailsService, UserDetailsPasswordService {

    @Autowired
    private EmployeeFeign employeeFeign;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthService authService;


    @Override
    public Employee loadUserByUsername(String username) throws UsernameNotFoundException {
       Employee employee = employeeFeign.loadUserByPhone(username);
       if (employee==null) throw new UsernameNotFoundException("未找到用户");//隐藏用户不存在并且隐藏springSecurity报错信息
       // 计算权限
       authService.compileAuth(employee);
       return employee;
    }
    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
       Employee employee = (Employee) user;
       return employeeFeign.update(new Employee().setId(employee.getId()).setCompanyId(employee.getCompanyId()).setPassword(newPassword));

    }
}

用户登录成功的监听器 InteractiveAuthenticationSuccessEvent 写入日志

public class LoginSuccessApplicationListener implements ApplicationListener<InteractiveAuthenticationSuccessEvent> {
    @Autowired
    private UserLogRecordService userLogRecordService;
    @Override
    @Async
    public void onApplicationEvent(InteractiveAuthenticationSuccessEvent event) {
        final UsernamePasswordAuthenticationToken source = (UsernamePasswordAuthenticationToken) event.getSource();
        SecurityContextHolder.getContext().setAuthentication(source);
        final String detail = ((CustomWebAuthenticationDetails) source.getDetails()).detail();
        source.setDetails(null);
        userLogRecordService.recordLogPure(UserLogOperatorType.ABOUT_USER, detail);
    }
}

自定义放置一些请求信息 authenticationDetailsSource


@Data
public class CustomWebAuthenticationDetails implements Serializable {
    private final String userAgentInfo;
    private final String ip;
    private final String sessionId;

    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        this.userAgentInfo = request.getHeader("user-agent");
        this.ip = IpUtils.getIpAddr(request);
        HttpSession session = request.getSession(false);
        this.sessionId = (session != null) ? session.getId() : null;
    }

    public CustomWebAuthenticationDetails() {
        this.ip = this.sessionId = this.userAgentInfo = null;
    }

    public CustomWebAuthenticationDetails(final String ip, final String sessionId, String userAgentInfo) {
        this.ip = ip;
        this.sessionId = sessionId;
        this.userAgentInfo = userAgentInfo;
    }


    public String getUserAgentInfo() {
        return userAgentInfo;
    }

    public String getIp() {
        return ip;
    }

    public String getSessionId() {
        return sessionId;
    }

    public String detail() {
        final UserAgentInfo userAgentInfo = UserAgentUtil.parse(getUserAgentInfo());
        if (userAgentInfo.hasOsInfo()) {
            return String.format("登录ip:%s,登录平台:%s,浏览器:%s", getIp(), userAgentInfo.getOsFamily(), userAgentInfo.getUaFamily());
        }
        return String.format("登录ip:%s,登录信息:%s", getIp(), getUserAgentInfo());
    }
}

登录传递其他参数 使用过滤器,或者传递用户名的时候拼接(username:companyId)当成用户名传递,在校验的时候再拆开 再或者自定义登录接口


public class PreLoginFilter extends OncePerRequestFilter {
    public static final ThreadLocal<LoginParameter> LoginParameter = ThreadLocal.withInitial(() -> null);
    private static final PathMatcher PATH_MATCHER = new AntPathMatcher();

    private boolean isProtectedUrl(HttpServletRequest request) {
        return "POST".equals(request.getMethod()) && PATH_MATCHER.match("/login", request.getServletPath());
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if(isProtectedUrl(request)) {
            CurrentCompanyIdHolder.set(request.getParameter("companyId"));
            LoginParameter.set(new LoginParameter(request));
        }
        filterChain.doFilter(request,response);
    }
    
}

当用户或者角色权限发生变化监听器

@Component
public class AuthModifyListener implements GenericApplicationListener {
    @Autowired
    private AuthService authService;
    @Autowired
    private EmployeeService userService;
    @Autowired
    private RedisSecurityContextRepository redisSecurityContextRepository;


    @Override
    public boolean supportsEventType(ResolvableType eventType) {
        return List.of(UserModifyEvent.class, RoleModifyEvent.class)
                .contains(eventType.getRawClass());
    }

    @Override
    @Async
    public void onApplicationEvent(@NonNull ApplicationEvent event) {
        if (event instanceof UserModifyEvent) {
            final Employee employee = ((UserModifyEvent) event).getSource();
            redisSecurityContextRepository.removeAuthenticationByUsername(List.of(employee.getUsername()));
            redisSecurityContextRepository.removeAuthentication(RedisKeyEnum.WEB_MOBILE_AGRICULTURAL_LBSS_TOKEN.getKey(), employee.getUsername());
        } else if (event instanceof RoleModifyEvent) {
            final EmployeeQuery userQuery = new EmployeeQuery();
            userQuery.setRoleIds(List.of(((RoleModifyEvent) event).getSource()));
            final List<Employee> content = userService.getPage(userQuery).getContent();
            redisSecurityContextRepository.removeAuthenticationByUsername(content.stream().map(Employee::getUsername).collect(Collectors.toList()));
        }else { //对于Auth的修改 去他妈的吧 让他们重新登录吧
            // 查出所有有次权限的人 踢出登录
            throw new HaveReasonException("未实现该事件的处理");
        }
    }
}

获取当前系统所有的接口 requestMappingHandlerMapping.getHandlerMethods() 或者看@GetMapping等接口 也可以反向看

@Service
public class WebURLService  {
    @Autowired
    private RequestMappingHandlerMapping requestMappingHandlerMapping;

    public List<RequestURl> extract() {
       Map<RequestMappingInfo, HandlerMethod> map = requestMappingHandlerMapping.getHandlerMethods();
       final List<RequestURl> list = map.entrySet().stream().map(x -> {
          String urlPattern = x.getKey().getPatternsCondition().getPatterns()
                .stream().findFirst().get().replaceAll("\{.*?\}", ".*");
          RequestMethod httpMethod = x.getKey().getMethodsCondition().
                getMethods().stream().findFirst().orElse(null);
          Operation operation = x.getValue().getMethodAnnotation(Operation.class);
          return new RequestURl(httpMethod,urlPattern,operation,null);
       }).filter(x-> Objects.nonNull(x.getMethod())).collect(Collectors.toList());
       return list;

    }

}

保存对象的redis

@Configuration
@ConditionalOnClass({ RedisTemplate.class })
public class MySecurityRedisConfig {


    @Bean
    @ConditionalOnMissingBean(name = "securityRedisTemplate")
    public RedisTemplate<String, Object> securityRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
       RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
       StringRedisSerializer keyRedisSerializer = new StringRedisSerializer(StandardCharsets.UTF_8);
       redisTemplate.setKeySerializer(keyRedisSerializer);
       redisTemplate.setHashKeySerializer(keyRedisSerializer);
       redisTemplate.setConnectionFactory(redisConnectionFactory);
       RedisSerializer<Object> redisSerializer = redisSerializer();
       redisTemplate.setValueSerializer(redisSerializer);
       redisTemplate.setHashValueSerializer(redisSerializer);
       return redisTemplate;
    }

    @Bean(name = { "springSessionDefaultRedisSerializer", "redisSerializer" })
    public RedisSerializer<Object> redisSerializer() {
       ObjectMapper objectMapper = new ObjectMapper();
       objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(),
             ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
       objectMapper
             .registerModules(SecurityJackson2Modules.getModules(SecurityJackson2Modules.class.getClassLoader()));
       objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
       objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
       objectMapper.registerModule(new JavaTimeModule());
       GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(
             objectMapper);
       return genericJackson2JsonRedisSerializer;
    }
}