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;
}
}