初始化
This commit is contained in:
24
pqs-auth/src/main/java/com/njcn/auth/AuthApplication.java
Normal file
24
pqs-auth/src/main/java/com/njcn/auth/AuthApplication.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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、刷新token:refresh-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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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) -> {
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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+userid,value为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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
29
pqs-auth/src/main/java/com/njcn/auth/utils/AuthPubUtil.java
Normal file
29
pqs-auth/src/main/java/com/njcn/auth/utils/AuthPubUtil.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user