初始化

This commit is contained in:
2022-06-21 20:47:46 +08:00
parent b666a24a98
commit 59da3376c1
1246 changed files with 129600 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
package com.njcn.auth;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author hongawen
* @version 1.0.0
* @date 2021年12月14日 20:33
*/
@Slf4j
@MapperScan("com.njcn.**.mapper")
@EnableFeignClients(basePackages = "com.njcn")
@SpringBootApplication(scanBasePackages = "com.njcn")
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class,args);
}
}

View File

@@ -0,0 +1,229 @@
package com.njcn.auth.config;
import cn.hutool.core.util.StrUtil;
import com.njcn.auth.filter.CustomClientCredentialsTokenEndpointFilter;
import com.njcn.auth.pojo.bo.BusinessUser;
import com.njcn.auth.security.clientdetails.ClientDetailsServiceImpl;
import com.njcn.auth.security.extension.captcha.CaptchaTokenGranter;
import com.njcn.auth.security.extension.refresh.PreAuthenticatedUserDetailsService;
import com.njcn.auth.service.UserDetailsServiceImpl;
import com.njcn.common.pojo.constant.SecurityConstants;
import com.njcn.common.pojo.enums.auth.ClientEnum;
import com.njcn.redis.utils.RedisUtil;
import com.njcn.user.enums.UserResponseEnum;
import com.njcn.web.utils.WebUtil;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import java.security.KeyPair;
import java.util.*;
/**
* @author hongawen
* @version 1.0.0
* @date 2021年05月11日 13:16
*/
@Configuration
@AllArgsConstructor
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
private final ClientDetailsServiceImpl clientDetailsService;
private final UserDetailsServiceImpl userDetailsService;
private final PasswordEncoder passwordEncoder;
private final RedisUtil redisUtil;
/**
* 客户端信息配置
*/
@Override
@SneakyThrows
public void configure(ClientDetailsServiceConfigurer clients) {
clients.withClientDetails(clientDetailsService);
}
/**
* 配置授权authorization以及令牌token的访问端点和令牌服务(token services)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
tokenEnhancers.add(tokenEnhancer());
tokenEnhancers.add(jwtAccessTokenConverter());
tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
// 获取原有默认授权模式(授权码模式、密码模式、客户端模式、简化模式)的授权者
List<TokenGranter> granterList = new ArrayList<>(Arrays.asList(endpoints.getTokenGranter()));
// 添加验证码授权模式授权者
granterList.add(new CaptchaTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
endpoints.getOAuth2RequestFactory(), authenticationManager, redisUtil
));
//todo... 后续可以扩展更多授权模式比如微信小程序、移动app
CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(granterList);
endpoints.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
//设置grant_type类型集合
.tokenEnhancer(tokenEnhancerChain)
.tokenGranter(compositeTokenGranter)
/**refresh_token有两种使用方式重复使用(true)、非重复使用(false)默认为true
*1.重复使用access_token过期刷新时 refresh token过期时间未改变仍以初次生成的时间为准
*2.非重复使用access_token过期刷新时 refresh_token过期时间延续在refresh_token有效期内刷新而无需失效再次登录
*/
.reuseRefreshTokens(true)
.tokenServices(tokenServices(endpoints));
}
public DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
tokenEnhancers.add(tokenEnhancer());
tokenEnhancers.add(jwtAccessTokenConverter());
tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setTokenEnhancer(tokenEnhancerChain);
// 多用户体系下刷新token再次认证客户端ID和 UserDetailService 的映射Map
Map<String, UserDetailsService> clientUserDetailsServiceMap = new HashMap<>();
// 系统管理客户端
clientUserDetailsServiceMap.put(ClientEnum.WEB_CLIENT.getClientId(), userDetailsService);
clientUserDetailsServiceMap.put(ClientEnum.WEB_CLIENT_TEST.getClientId(), userDetailsService);
clientUserDetailsServiceMap.put(ClientEnum.APP_CLIENT.getClientId(), userDetailsService);
clientUserDetailsServiceMap.put(ClientEnum.SCREEN_CLIENT.getClientId(), userDetailsService);
clientUserDetailsServiceMap.put(ClientEnum.WE_CHAT_APP_CLIENT.getClientId(), userDetailsService);
//todo .. 后面扩展微信小程序、app实现服务
// 刷新token模式下重写预认证提供者替换其AuthenticationManager可自定义根据客户端ID和认证方式区分用户体系获取认证用户信息
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new PreAuthenticatedUserDetailsService<>(clientUserDetailsServiceMap));
tokenServices.setAuthenticationManager(new ProviderManager(Collections.singletonList(provider)));
return tokenServices;
}
/**
* 使用非对称加密算法对token签名
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyPair());
return converter;
}
/**
* 从classpath下的密钥库中获取密钥对(公钥+私钥)
*/
@Bean
public KeyPair keyPair() {
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("njcn.jks"), "njcnpqs".toCharArray());
return factory.getKeyPair("njcn", "njcnpqs".toCharArray());
}
/**
* 自定义认证异常响应数据
*/
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return (request, response, e) -> {
WebUtil.responseInfo(response, UserResponseEnum.CLIENT_AUTHENTICATION_FAILED.getCode(), UserResponseEnum.CLIENT_AUTHENTICATION_FAILED.getMessage());
};
}
/**
* JWT内容增强
*/
@Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
String clientId = authentication.getOAuth2Request().getClientId();
BusinessUser user = (BusinessUser) authentication.getUserAuthentication().getPrincipal();
Map<String, Object> map = new HashMap<>(8);
map.put(SecurityConstants.USER_INDEX_KEY, user.getUserIndex());
map.put(SecurityConstants.USER_TYPE, user.getType());
map.put(SecurityConstants.USER_NICKNAME_KEY, user.getNickName());
map.put(SecurityConstants.CLIENT_ID_KEY, clientId);
map.put(SecurityConstants.DEPT_INDEX_KEY, user.getDeptIndex());
if (StrUtil.isNotBlank(user.getAuthenticationMethod())) {
map.put(SecurityConstants.AUTHENTICATION_METHOD, user.getAuthenticationMethod());
}
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
return accessToken;
};
}
/**
* 配置自定义密码认证过滤器
* @param security .
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
CustomClientCredentialsTokenEndpointFilter endpointFilter = new CustomClientCredentialsTokenEndpointFilter(security);
endpointFilter.afterPropertiesSet();
endpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint());
security.addTokenEndpointAuthenticationFilter(endpointFilter);
security
.authenticationEntryPoint(authenticationEntryPoint())
/* .allowFormAuthenticationForClients()*/ //如果使用表单认证则需要加上
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
}

View File

@@ -0,0 +1,104 @@
package com.njcn.auth.config;
import com.njcn.auth.security.sm4.Sm4AuthenticationProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author hongawen
*/
@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService sysUserDetailsService;
private final Sm4AuthenticationProvider sm4AuthenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/oauth/getPublicKey","/oauth/logout","/auth/getImgCode","/judgeToken/guangZhou").permitAll()
// @link https://gitee.com/xiaoym/knife4j/issues/I1Q5X6 (接口文档knife4j需要放行的规则)
.antMatchers("/webjars/**","/doc.html","/swagger-resources/**","/v2/api-docs").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
/**
* 认证管理对象
*
* @throws Exception .
* @return .
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(daoAuthenticationProvider());
}
/**
* 用户名密码认证授权提供者
*
* @return
*/
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(sysUserDetailsService);
provider.setPasswordEncoder(passwordEncoder());
provider.setHideUserNotFoundExceptions(false); // 是否隐藏用户不存在异常,默认:true-隐藏false-抛出异常;
return provider;
}
/**
* 重写父类自定义AuthenticationManager 将provider注入进去
* 当然我们也可以考虑不重写 在父类的manager里面注入provider
*/
@Bean
@Override
protected AuthenticationManager authenticationManager(){
return new ProviderManager(sm4AuthenticationProvider);
}
/**
* 密码编码器
* <p>
* 委托方式根据密码的前缀选择对应的encoder例如{bcypt}前缀->标识BCYPT算法加密{noop}->标识不使用任何加密即明文的方式
* 密码判读 DaoAuthenticationProvider#additionalAuthenticationChecks
*/
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}

View File

@@ -0,0 +1,140 @@
package com.njcn.auth.controller;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.njcn.auth.service.UserTokenService;
import com.njcn.common.pojo.constant.SecurityConstants;
import com.njcn.common.pojo.dto.UserTokenInfo;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.HttpResultUtil;
import com.njcn.common.utils.LogUtil;
import com.njcn.common.utils.sm.DesUtils;
import com.njcn.redis.utils.RedisUtil;
import com.njcn.user.api.UserFeignClient;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.RequestUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.*;
import springfox.documentation.annotations.ApiIgnore;
import java.security.KeyPair;
import java.security.Principal;
import java.security.interfaces.RSAPublicKey;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @author hongawen
*/
@Api(tags = "认证中心")
@Slf4j
@RestController
@RequestMapping("/oauth")
@AllArgsConstructor
public class AuthController extends BaseController {
private final TokenEndpoint tokenEndpoint;
private final KeyPair keyPair;
private final RedisUtil redisUtil;
private final UserFeignClient userFeignClient;
private final UserTokenService userTokenService;
@ApiIgnore
@ApiOperation("登录认证")
@ApiImplicitParams({
@ApiImplicitParam(name = SecurityConstants.GRANT_TYPE, defaultValue = "password", value = "授权模式", required = true),
@ApiImplicitParam(name = SecurityConstants.CLIENT_ID, defaultValue = "njcn", value = "Oauth2客户端ID", required = true),
@ApiImplicitParam(name = SecurityConstants.CLIENT_SECRET, defaultValue = "njcnpqs", value = "Oauth2客户端秘钥", required = true),
@ApiImplicitParam(name = SecurityConstants.REFRESH_TOKEN, value = "刷新token"),
@ApiImplicitParam(name = SecurityConstants.USERNAME, value = "登录用户名"),
@ApiImplicitParam(name = SecurityConstants.PASSWORD, value = "登录密码"),
@ApiImplicitParam(name = SecurityConstants.IMAGE_CODE, value = "图形验证码"),
})
@PostMapping("/token")
public Object postAccessToken(@ApiIgnore Principal principal, @RequestParam @ApiIgnore Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
String methodDescribe = getMethodDescribe("postAccessToken");
String username = parameters.get(SecurityConstants.USERNAME);
String grantType = parameters.get(SecurityConstants.GRANT_TYPE);
//正式环境需删除,均是加密的用户名
if (!grantType.equalsIgnoreCase(SecurityConstants.PASSWORD)) {
username = DesUtils.aesDecrypt(username);
}
if (grantType.equalsIgnoreCase(SecurityConstants.REFRESH_TOKEN_KEY)) {
//如果是刷新token需要去黑名单校验
userTokenService.judgeRefreshToken(parameters.get(SecurityConstants.REFRESH_TOKEN_KEY));
}
RequestUtil.saveLoginName(username);
OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
//用户的登录名&密码校验成功后,判断当前该用户是否可以正常使用系统
userFeignClient.judgeUserStatus(username);
//登录成功后记录token信息并处理踢人效果
userTokenService.recordUserInfo(oAuth2AccessToken);
if (!grantType.equalsIgnoreCase(SecurityConstants.PASSWORD)) {
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, oAuth2AccessToken, methodDescribe);
} else {
return oAuth2AccessToken;
}
}
@ApiOperation("用户登出系统")
@DeleteMapping("/logout")
public HttpResult<Object> logout() {
String methodDescribe = getMethodDescribe("logout");
String userIndex = RequestUtil.getUserIndex();
String username = RequestUtil.getUsername();
LogUtil.njcnDebug(log, "{},用户名为:{}", methodDescribe, username);
String blackUserKey = SecurityConstants.TOKEN_BLACKLIST_PREFIX + userIndex;
String onlineUserKey = SecurityConstants.TOKEN_ONLINE_PREFIX + userIndex;
Object onlineTokenInfoOld = redisUtil.getObjectByKey(onlineUserKey);
List<UserTokenInfo> blackUsers = (List<UserTokenInfo>) redisUtil.getObjectByKey(blackUserKey);
UserTokenInfo userTokenInfo;
if (!Objects.isNull(onlineTokenInfoOld)) {
//清除在线token信息
redisUtil.delete(onlineUserKey);
userTokenInfo = (UserTokenInfo) onlineTokenInfoOld;
if (CollectionUtils.isEmpty(blackUsers)) {
blackUsers = new ArrayList<>();
}
blackUsers.add(userTokenInfo);
LocalDateTime refreshTokenExpire = userTokenInfo.getRefreshTokenExpire();
long lifeTime = Math.abs(refreshTokenExpire.plusMinutes(5L).toEpochSecond(ZoneOffset.of("+8")) - LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8")));
redisUtil.saveByKeyWithExpire(blackUserKey, blackUsers, lifeTime);
}
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, null, methodDescribe);
}
/**
* 文档隐藏该接口
*/
@ApiIgnore
@ApiOperation("RSA公钥获取接口")
@GetMapping("/getPublicKey")
public Map<String, Object> getPublicKey() {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
}
}

View File

@@ -0,0 +1,77 @@
package com.njcn.auth.controller;
import cn.hutool.json.JSONObject;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.HttpResultUtil;
import com.njcn.common.utils.LogUtil;
import com.njcn.web.controller.BaseController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
/**
* @author hongawen
* @version 1.0.0
* @date 2022年04月27日 11:22
*/
@Slf4j
@RestController
@AllArgsConstructor
@Api(tags = "校验第三方token")
@RequestMapping("/judgeToken")
public class JudgeThirdToken extends BaseController {
/**
* 校验广州超高压token有效性
*
* @param token token数据
*/
@OperateInfo(info = LogEnum.SYSTEM_COMMON)
@PostMapping("/guangZhou")
@ApiOperation("校验广州超高压token有效性")
@ApiImplicitParam(name = "token", value = "", required = true)
public HttpResult<Object> guangZhou(String token) {
RestTemplate restTemplate = new RestTemplate();
String methodDescribe = getMethodDescribe("guangZhou");
LogUtil.njcnDebug(log, "{}token{}", methodDescribe, token);
// 请求地址
String url = "http://10.121.17.9:9080/ehv/auth_valid";
// 请求头设置,x-www-form-urlencoded格式的数据
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
//提交参数设置
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("token", token);
// 组装请求体
HttpEntity<MultiValueMap<String, String>> request =
new HttpEntity<>(map, headers);
// 发送post请求并打印结果以String类型接收响应结果JSON字符串
String result = restTemplate.postForObject(url, request, String.class);
JSONObject resultJson = new JSONObject(result);
System.out.println(result);
if (resultJson.getInt("status") == 1) {
//成功
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, null, methodDescribe);
} else {
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, null, methodDescribe);
}
}
}

View File

@@ -0,0 +1,80 @@
package com.njcn.auth.controller;
import cn.hutool.core.io.IoUtil;
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.util.Config;
import com.njcn.auth.utils.AuthPubUtil;
import com.njcn.common.pojo.constant.SecurityConstants;
import com.njcn.redis.utils.RedisUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import springfox.documentation.annotations.ApiIgnore;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Properties;
/**
* @author hongawen
* @version 1.0.0
* @date 2021年06月04日 15:25
*/
@Api(tags = "认证中心")
@Slf4j
@Controller
@RequestMapping("/auth")
@AllArgsConstructor
public class KaptchaController {
private final RedisUtil redisUtil;
@ApiIgnore
@ApiOperation("获取图形验证码")
@GetMapping("/getImgCode")
public void getImgCode(@ApiIgnore HttpServletResponse resp, @ApiIgnore HttpServletRequest request) {
ServletOutputStream out = null;
try {
out = resp.getOutputStream();
// resp.setContentType("image/jpeg");"/pqs-auth/auth/getImgCode",
if (null != out) {
Properties props = new Properties();
Producer kaptchaProducer;
ImageIO.setUseCache(false);
props.put("kaptcha.border", "no");
props.put("kaptcha.textproducer.font.color", "black");
/*props.put("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.ShadowGimpy");*/
/*props.put("kaptcha.noise.impl", "com.sso.utils.ComplexNoise");*/
props.put("kaptcha.textproducer.char.space", "5");
props.put("kaptcha.textproducer.char.length", "4");
Config config = new Config(props);
kaptchaProducer = config.getProducerImpl();
//此处需要固定采用字母和数字混合
String capText = AuthPubUtil.getKaptchaText(4);
String userAgent = request.getHeader(HttpHeaders.USER_AGENT);
String ip = request.getHeader(SecurityConstants.REQUEST_HEADER_KEY_CLIENT_REAL_IP);
String key = userAgent + ip;
redisUtil.delete(key);
redisUtil.saveByKeyWithExpire(key, capText, 30*60L);
BufferedImage bi = kaptchaProducer.createImage(capText);
ImageIO.write(bi, "jpg", out);
out.flush();
}
} catch (IOException ioException) {
log.error("获取图形验证码异常,异常为:{}", ioException.toString());
} finally {
IoUtil.close(out);
}
}
}

View File

@@ -0,0 +1,88 @@
package com.njcn.auth.exception;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.nimbusds.jose.JWSObject;
import com.njcn.common.pojo.constant.LogInfo;
import com.njcn.common.pojo.constant.SecurityConstants;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.HttpResultUtil;
import com.njcn.user.api.UserFeignClient;
import com.njcn.user.enums.UserResponseEnum;
import com.njcn.web.service.ILogService;
import com.njcn.web.utils.RequestUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.common.exceptions.UnsupportedGrantTypeException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* @author hongawen
* @version 1.0.0
* @date 2021年05月17日 12:46
*/
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class AuthExceptionHandler {
private final UserFeignClient userFeignClient;
private final ILogService logService;
/**
* 用户名和密码非法
*/
@ExceptionHandler(InvalidGrantException.class)
public HttpResult<String> handleInvalidGrantException(HttpServletRequest httpServletRequest, InvalidGrantException invalidGrantException) {
String loginName = invalidGrantException.getMessage();
logService.recodeAuthExceptionLog(invalidGrantException, httpServletRequest, UserResponseEnum.LOGIN_WRONG_PWD.getMessage(), loginName);
HttpResult<String> result = userFeignClient.updateUserLoginErrorTimes(loginName);
if (result.getData().equals(UserResponseEnum.LOGIN_USER_LOCKED.getMessage())) {
return HttpResultUtil.assembleResult(UserResponseEnum.LOGIN_USER_LOCKED.getCode(), null, UserResponseEnum.LOGIN_USER_LOCKED.getMessage());
} else {
return HttpResultUtil.assembleResult(UserResponseEnum.LOGIN_WRONG_PWD.getCode(), null, UserResponseEnum.LOGIN_WRONG_PWD.getMessage());
}
}
/**
* 不支持的认证方式
* <p>
* 不支持的认证方式 目前支持用户名密码password、刷新tokenrefresh-token
*/
@ExceptionHandler(UnsupportedGrantTypeException.class)
public HttpResult<String> unsupportedGrantTypeExceptionException(HttpServletRequest httpServletRequest, UnsupportedGrantTypeException unsupportedGrantTypeException) {
String loginName = RequestUtil.getLoginName(httpServletRequest);
logService.recodeAuthExceptionLog(unsupportedGrantTypeException, httpServletRequest, UserResponseEnum.UNSUPPORTED_GRANT_TYPE.getMessage(), loginName);
return HttpResultUtil.assembleResult(UserResponseEnum.UNSUPPORTED_GRANT_TYPE.getCode(), null, UserResponseEnum.UNSUPPORTED_GRANT_TYPE.getMessage());
}
/**
* oAuth2中token校验异常
*/
@SneakyThrows
@ExceptionHandler(InvalidTokenException.class)
public HttpResult<String> invalidTokenExceptionException(HttpServletRequest httpServletRequest, InvalidTokenException invalidTokenException) {
final String EXPIRED_KEY = "Invalid refresh token (expired):";
if (invalidTokenException.getMessage().startsWith(EXPIRED_KEY)) {
String message = invalidTokenException.getMessage();
message = message.substring(EXPIRED_KEY.length());
JWSObject jwsObject = JWSObject.parse(message);
String payload = jwsObject.getPayload().toString();
JSONObject jsonObject = JSONUtil.parseObj(payload);
logService.recodeAuthExceptionLog(invalidTokenException, httpServletRequest, UserResponseEnum.REFRESH_TOKEN_EXPIRE_JWT.getMessage(), jsonObject.getStr(SecurityConstants.USER_NAME_KEY));
return HttpResultUtil.assembleResult(UserResponseEnum.REFRESH_TOKEN_EXPIRE_JWT.getCode(), null, UserResponseEnum.REFRESH_TOKEN_EXPIRE_JWT.getMessage());
}
logService.recodeAuthExceptionLog(invalidTokenException, httpServletRequest, UserResponseEnum.PARSE_TOKEN_FORBIDDEN_JWT.getMessage(), LogInfo.UNKNOWN_USER);
return HttpResultUtil.assembleResult(UserResponseEnum.PARSE_TOKEN_FORBIDDEN_JWT.getCode(), null, UserResponseEnum.PARSE_TOKEN_FORBIDDEN_JWT.getMessage());
}
}

View File

@@ -0,0 +1,42 @@
package com.njcn.auth.filter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter;
import org.springframework.security.web.AuthenticationEntryPoint;
/**
* @author hongawen
* @version 1.0.0
* @createTime 2021年05月24日 15:39
*/
public class CustomClientCredentialsTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter {
private final AuthorizationServerSecurityConfigurer configurer;
private AuthenticationEntryPoint authenticationEntryPoint;
public CustomClientCredentialsTokenEndpointFilter(AuthorizationServerSecurityConfigurer configurer) {
this.configurer = configurer;
}
@Override
public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
super.setAuthenticationEntryPoint(null);
this.authenticationEntryPoint = authenticationEntryPoint;
}
@Override
protected AuthenticationManager getAuthenticationManager() {
return configurer.and().getSharedObject(AuthenticationManager.class);
}
@Override
public void afterPropertiesSet() {
setAuthenticationFailureHandler((request, response, e) -> authenticationEntryPoint.commence(request, response, e));
setAuthenticationSuccessHandler((request, response, authentication) -> {
});
}
}

View File

@@ -0,0 +1,94 @@
package com.njcn.auth.pojo.bo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
/**
* @author hongawen
* @version 1.0.0
* @createTime 2021年04月28日 13:31
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BusinessUser implements UserDetails {
private String userIndex;
private String username;
private String nickName;
private String password;
private String clientId;
private String deptIndex;
private Collection<? extends GrantedAuthority> authorities;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
private String secretKey;
private String standBy;
private String authenticationMethod;
private Integer type;
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities(){
return authorities;
}
public BusinessUser(String username, String password, List<GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities =authorities;
}
}

View File

@@ -0,0 +1,58 @@
package com.njcn.auth.security.clientdetails;
import com.njcn.common.pojo.enums.auth.PasswordEncoderTypeEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.user.api.AuthClientFeignClient;
import com.njcn.user.pojo.po.AuthClient;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.NoSuchClientException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.stereotype.Service;
/**
* OAuth2 客户端信息
* @author hongawen
*/
@Service
@RequiredArgsConstructor
public class ClientDetailsServiceImpl implements ClientDetailsService {
private final AuthClientFeignClient authClientFeignClient;
@Override
public ClientDetails loadClientByClientId(String clientName) {
try {
HttpResult<AuthClient> authClientResult = authClientFeignClient.getAuthClientByName(clientName);
AuthClient authClient = authClientResult.getData();
BaseClientDetails clientDetails = new BaseClientDetails(
authClient.getName(),
authClient.getResourceIds(),
authClient.getScope(),
authClient.getAuthorizedGrantTypes(),
authClient.getAuthorities(),
authClient.getWebServerRedirectUri()
);
clientDetails.setClientSecret(PasswordEncoderTypeEnum.BCRYPT.getPrefix() + authClient.getClientSecret());
clientDetails.setAccessTokenValiditySeconds(authClient.getAccessTokenValidity());
clientDetails.setRefreshTokenValiditySeconds(authClient.getRefreshTokenValidity());
return clientDetails;
} catch (EmptyResultDataAccessException var4) {
throw new NoSuchClientException("No client with requested id: " + clientName);
}
}
public static void main(String[] args) {
PasswordEncoder delegatingPasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String njcnpqs = delegatingPasswordEncoder.encode("njcnpqs");
//{bcrypt}$2a$10$xIP3g5Rc11zDdclsKXpQXuOobvZ9gaw2Mix1rkOm1MJN1.hTVY7ci
System.out.println(njcnpqs);
System.out.println(delegatingPasswordEncoder.matches("njcnpqs","{bcrypt}$2a$10$xIP3g5Rc11zDdclsKXpQXuOobvZ9gaw2Mix1rkOm1MJN1.hTVY7ci"));
}
}

View File

@@ -0,0 +1,101 @@
package com.njcn.auth.security.extension.captcha;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.constant.SecurityConstants;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.common.utils.sm.DesUtils;
import com.njcn.common.utils.sm.Sm2;
import com.njcn.redis.utils.RedisUtil;
import com.njcn.user.enums.UserResponseEnum;
import com.njcn.web.utils.RequestUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import javax.servlet.http.HttpServletRequest;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author hongawen
* @version 1.0.0
* @date 2021年12月15日 14:23
*/
@Slf4j
public class CaptchaTokenGranter extends AbstractTokenGranter {
private final AuthenticationManager authenticationManager;
private final RedisUtil redisUtil;
public CaptchaTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager,
RedisUtil redisUtil
) {
//SecurityConstants.GRANT_CAPTCHA申明为授权码模式
super(tokenServices, clientDetailsService, requestFactory, SecurityConstants.GRANT_CAPTCHA);
this.authenticationManager = authenticationManager;
this.redisUtil = redisUtil;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
String username = parameters.get(SecurityConstants.USERNAME);
username = DesUtils.aesDecrypt(username);
if (!judgeImageCode(parameters.get(SecurityConstants.IMAGE_CODE), RequestUtil.getRequest())) {
throw new BusinessException(UserResponseEnum.LOGIN_WRONG_CODE);
}
String password = parameters.get(SecurityConstants.PASSWORD);
String ip = RequestUtil.getRequest().getHeader(SecurityConstants.REQUEST_HEADER_KEY_CLIENT_REAL_IP);
//密码处理
String privateKey = redisUtil.getStringByKey(username + ip);
// //秘钥用完即删
redisUtil.delete(username + ip);
//对SM2解密面进行验证
password = Sm2.getPasswordSM2Verify(privateKey, password);
if (StrUtil.isBlankIfStr(password)) {
throw new BusinessException(UserResponseEnum.PASSWORD_TRANSPORT_ERROR);
}
//正式环境放行
parameters.remove(SecurityConstants.PASSWORD);
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager.authenticate(userAuth);
} catch (AccountStatusException | BadCredentialsException ase) {
//covers expired, locked, disabled cases
throw new InvalidGrantException(ase.getMessage());
}
// If the username/password are wrong the spec says we should send 400/invalid grant
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user: " + username);
}
OAuth2Request storedOauth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOauth2Request, userAuth);
}
/**
* @param imageCode 图形验证码
*/
private boolean judgeImageCode(String imageCode, HttpServletRequest request) {
if (StrUtil.isBlankIfStr(imageCode)) {
return false;
}
String userAgent = request.getHeader(HttpHeaders.USER_AGENT);
String ip = request.getHeader(SecurityConstants.REQUEST_HEADER_KEY_CLIENT_REAL_IP);
String key = userAgent + ip;
String redisImageCode = redisUtil.getStringByKey(key);
if (imageCode.equalsIgnoreCase(redisImageCode)) {
redisUtil.delete(key);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,60 @@
package com.njcn.auth.security.extension.refresh;
import com.njcn.common.pojo.constant.SecurityConstants;
import com.njcn.common.pojo.enums.auth.AuthenticationMethodEnum;
import com.njcn.web.utils.RequestUtil;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import java.util.Map;
/**
* 刷新token再次认证 UserDetailsService
*
* @author hongawen
* @date 2021/10/2
*/
@NoArgsConstructor
public class PreAuthenticatedUserDetailsService<T extends Authentication> implements AuthenticationUserDetailsService<T>, InitializingBean {
/**
* 客户端ID和用户服务 UserDetailService 的映射
*
* @see com.njcn.auth.config.AuthorizationServerConfig#tokenServices(AuthorizationServerEndpointsConfigurer)
*/
private Map<String, UserDetailsService> userDetailsServiceMap;
public PreAuthenticatedUserDetailsService(Map<String, UserDetailsService> userDetailsServiceMap) {
Assert.notNull(userDetailsServiceMap, "userDetailsService cannot be null.");
this.userDetailsServiceMap = userDetailsServiceMap;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.userDetailsServiceMap, "UserDetailsService must be set");
}
/**
* 重写PreAuthenticatedAuthenticationProvider 的 preAuthenticatedUserDetailsService 属性,可根据客户端和认证方式选择用户服务 UserDetailService 获取用户信息 UserDetail
*
* @param authentication .
* @return .
* @throws UsernameNotFoundException .
*/
@Override
public UserDetails loadUserDetails(T authentication) throws UsernameNotFoundException {
String clientId = RequestUtil.getOAuth2ClientId();
// 获取认证方式,默认是用户名 username
UserDetailsService userDetailsService = userDetailsServiceMap.get(clientId);
return userDetailsService.loadUserByUsername(authentication.getName());
}
}

View File

@@ -0,0 +1,92 @@
package com.njcn.auth.security.sm4;
import com.njcn.auth.pojo.bo.BusinessUser;
import com.njcn.common.utils.sm.Sm4Utils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
/**
* @author hongawen
* @version 1.0.0
* @date 2021年06月08日 15:43
*/
@Slf4j
@Component
@AllArgsConstructor
public class Sm4AuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private final UserDetailsService userDetailsService;
/**
* 校验密码有效性.
*
* @param userDetails 用户详细信息
* @param authentication 用户登录的密码
* @throws AuthenticationException .
*/
@Override
protected void additionalAuthenticationChecks(
UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
BusinessUser businessUser = (BusinessUser)userDetails;
String secretKey = businessUser.getSecretKey();
Sm4Utils sm4 = new Sm4Utils(secretKey);
//SM4加密密码
String sm4PwdOnce = sm4.encryptData_ECB(presentedPassword);
//SM4加密(密码+工作秘钥)
String sm4PwdTwice = sm4.encryptData_ECB(sm4PwdOnce + secretKey);
if(!businessUser.getPassword().equalsIgnoreCase(sm4PwdTwice)){
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
businessUser.getUsername()));
}
}
/**
* 获取用户
*
* @param username 用户名
* @param authentication 认证token
* @throws AuthenticationException .
*/
@Override
protected UserDetails retrieveUser(
String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser = userDetailsService.loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
/**
* 授权持久化.
*/
@Override
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
return super.createSuccessAuthentication(principal, authentication, user);
}
}

View File

@@ -0,0 +1,47 @@
package com.njcn.auth.service;
import cn.hutool.core.bean.BeanUtil;
import com.njcn.auth.pojo.bo.BusinessUser;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil;
import com.njcn.user.api.UserFeignClient;
import com.njcn.user.pojo.dto.UserDTO;
import com.njcn.web.utils.RequestUtil;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* @author hongawen
* <p>
* 自定义用户认证和授权
*/
@Slf4j
@Service
@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserFeignClient userFeignClient;
@SneakyThrows
@Override
public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {
String clientId = RequestUtil.getOAuth2ClientId();
BusinessUser businessUser = new BusinessUser(loginName, null, null);
businessUser.setClientId(clientId);
HttpResult<UserDTO> result = userFeignClient.getUserByName(loginName);
LogUtil.njcnDebug(log, "用户认证时,用户名:{}获取用户信息:{}", loginName, result.toString());
//成功获取用户信息
UserDTO userDTO = result.getData();
BeanUtil.copyProperties(userDTO,businessUser,true);
businessUser.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", userDTO.getRoleName())));
return businessUser;
}
}

View File

@@ -0,0 +1,121 @@
package com.njcn.auth.service;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.nimbusds.jose.JWSObject;
import com.njcn.common.pojo.constant.SecurityConstants;
import com.njcn.common.pojo.dto.UserTokenInfo;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.redis.utils.RedisUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.stereotype.Service;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import java.text.ParseException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
/**
* @author hongawen
* @version 1.0.0
* @date 2022年03月11日 10:34
*/
@Slf4j
@Service
@AllArgsConstructor
public class UserTokenService {
private final RedisUtil redisUtil;
/**
* 记录用户token信息并经过处理后达到最新登录的使用者将之前的token信息置为黑名单过期状态
* 1、从在线名单中获取该用户的token信息key为:TOKEN_ONLINE_PREFIX+useridvalue为userTokenInfo的json对象
* 1.1 有,则表示有人使用该账户登录过
* 1.1.1 将在线名单的用户信息添加到黑名单并清除黑名单中已经过期的token信息
* 重新赋值黑名单信息key为TOKEN_BLACKLIST_PREFIX+userid,value为userTokenInfo的集合
* 1.2 没有该账号当前只有本人在登录将当前token等信息保存到白名单
*
* @param oAuth2AccessToken 认证后的最新token信息
*/
@Async("asyncExecutor")
public void recordUserInfo(OAuth2AccessToken oAuth2AccessToken) {
UserTokenInfo userTokenInfo = new UserTokenInfo();
String accessTokenValue = oAuth2AccessToken.getValue();
JWSObject accessJwsObject ;
try {
accessJwsObject = JWSObject.parse(accessTokenValue);
} catch (ParseException e) {
throw new BusinessException(CommonResponseEnum.PARSE_TOKEN_ERROR);
}
JSONObject accessJson = JSONUtil.parseObj(accessJwsObject.getPayload().toString());
String userIndex = accessJson.getStr(SecurityConstants.USER_INDEX_KEY);
//查询是否有在线的当前用户
String onlineUserKey = SecurityConstants.TOKEN_ONLINE_PREFIX + userIndex;
Object onlineTokenInfoOld = redisUtil.getObjectByKey(onlineUserKey);
if (!Objects.isNull(onlineTokenInfoOld)) {
//存在在线用户,将在线用户添加到黑名单列表
String blackUserKey = SecurityConstants.TOKEN_BLACKLIST_PREFIX + userIndex;
List<UserTokenInfo> blackUsers = (List<UserTokenInfo>) redisUtil.getObjectByKey(blackUserKey);
if (CollectionUtils.isEmpty(blackUsers)) {
blackUsers = new ArrayList<>();
}
blackUsers.add((UserTokenInfo) onlineTokenInfoOld);
//筛选黑名单中是否存在过期的token信息
blackUsers.removeIf(userTokenInfoTemp -> userTokenInfoTemp.getRefreshTokenExpire().isBefore(LocalDateTime.now()));
//将黑名单集合重新缓存此处根据最新的黑名单计算当前这个key的生命周期在时间差的基础上增加5分钟的延迟时间
LocalDateTime refreshTokenExpire = ((UserTokenInfo) onlineTokenInfoOld).getRefreshTokenExpire();
long lifeTime = Math.abs(refreshTokenExpire.plusMinutes(5L).toEpochSecond(ZoneOffset.of("+8")) - LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8")));
redisUtil.saveByKeyWithExpire(blackUserKey, blackUsers, lifeTime);
}
String accessJti = accessJson.getStr(SecurityConstants.JWT_JTI);
OAuth2RefreshToken refreshToken = oAuth2AccessToken.getRefreshToken();
JWSObject refreshJwsObject ;
try {
refreshJwsObject = JWSObject.parse(refreshToken.getValue());
} catch (ParseException e) {
throw new BusinessException(CommonResponseEnum.PARSE_TOKEN_ERROR);
}
JSONObject refreshJson = JSONUtil.parseObj(refreshJwsObject.getPayload().toString());
String refreshJti = refreshJson.getStr(SecurityConstants.JWT_JTI);
Long refreshExpireTime = refreshJson.getLong(SecurityConstants.JWT_EXP);
userTokenInfo.setAccessTokenJti(accessJti);
userTokenInfo.setRefreshToken(refreshToken.getValue());
LocalDateTime refreshLifeTime =LocalDateTime.ofEpochSecond(refreshExpireTime,0,ZoneOffset.of("+8"));
userTokenInfo.setRefreshTokenExpire(refreshLifeTime);
//生命周期在refreshToken的基础上延迟5分钟
redisUtil.saveByKeyWithExpire(onlineUserKey, userTokenInfo, refreshLifeTime.plusMinutes(5L).toEpochSecond(ZoneOffset.of("+8")) - LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8")));
}
/**
* 校验刷新token是否被加入黑名单
*
* @param refreshToken 刷新token
*/
public void judgeRefreshToken(String refreshToken) {
JWSObject refreshJwsObject;
try {
refreshJwsObject = JWSObject.parse(refreshToken);
} catch (ParseException e) {
throw new BusinessException();
}
JSONObject refreshJson = JSONUtil.parseObj(refreshJwsObject.getPayload().toString());
String userIndex = refreshJson.getStr(SecurityConstants.USER_INDEX_KEY);
String blackUserKey = SecurityConstants.TOKEN_BLACKLIST_PREFIX + userIndex;
List<UserTokenInfo> blackUsers = (List<UserTokenInfo>) redisUtil.getObjectByKey(blackUserKey);
if (CollectionUtils.isNotEmpty(blackUsers)) {
blackUsers.forEach(temp -> {
//存在当前的刷新token则抛出业务异常
if(temp.getRefreshToken().equalsIgnoreCase(refreshToken)){
throw new BusinessException(CommonResponseEnum.TOKEN_EXPIRE_JWT);
}
});
}
}
}

View File

@@ -0,0 +1,29 @@
package com.njcn.auth.utils;
import cn.hutool.core.util.RandomUtil;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* @author hongawen
* @version 1.0.0
* @date 2021年06月04日 14:00
*/
public class AuthPubUtil {
public static String getKaptchaText(int codeLength) {
StringBuilder code = new StringBuilder();
int letterLength = RandomUtil.randomInt(codeLength - 1) + 1;
code.append(RandomUtil.randomString(RandomUtil.BASE_CHAR, letterLength).toUpperCase(Locale.ROOT));
int numberLength = codeLength - letterLength;
code.append(RandomUtil.randomString(RandomUtil.BASE_NUMBER, numberLength));
List<String> textList = Arrays.asList(code.toString().split(""));
//填充完字符后,打乱顺序,返回字符串
Collections.shuffle(textList);
return String.join("", textList);
}
}