#user nobody;
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
# nginx: [emerg] could not build server_names_hash, you should increase server_names_hash_bucket_size: 32 解决此错误需要增加下两行配置
server_names_hash_max_size 2048;# 【值为域名长度总和】;
server_names_hash_bucket_size 2048;# 【上升值】
# /sockjs-node 访问异常->
# 参考:https://blog.csdn.net/qq27229639/article/details/103069055
# https://www.ancii.com/anbgjpemb
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name www.authorization.life;
rewrite ^(.*)$ https://$server_name$1 permanent;
}
# HTTPS server 参考配置: https://nenufm.com/archives/g
server {
listen 443 ssl;
server_name www.authorization.life;
ssl_certificate D://authorization_life_aliyun//authorization_life_chain.pem;
ssl_certificate_key D://authorization_life_aliyun//authorization_life_key.key;
# ssl验证相关配置
ssl_protocols TLSv1.3 SSLv3; #安全链接可选的加密协议
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1; #为ECDHE密码指定 SEPO384Q1
ssl_session_timeout 10m; #缓存有效期
ssl_session_cache shared:SSL:10m;
ssl_prefer_server_ciphers on;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
#后端服务gateway
location / {
proxy_pass http://127.0.0.1:9000;
}
# 前端登录工程
location /login {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
}
2.7.3
2021.0.3
2.2.8.RELEASE
2.13.3
5.7.3
9.23
9.27
0.3.1
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
com.nimbusds
oauth2-oidc-sdk
${oauth2-oidc-sdk.version}
com.nimbusds
nimbus-jose-jwt
${nimbus-jose-jwt.version}
org.springframework.security
spring-security-oauth2-authorization-server
${oauth2-authorization-server.version}
com.fasterxml.jackson
jackson-bom
${jackson-bom.version}
pom
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${spring-cloud-alibaba.version}
pom
import
org.springframework.security
spring-security-bom
${spring-security.version}
pom
import
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
OAuth2TokenCustomizer<JwtEncodingContext> oAuth2TokenCustomizer) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
// 配置请求拦截
http.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests -> authorizeRequests
// // 无需认证即可访问
.antMatchers(SecurityConstant.IGNORE_PERM_URLS).permitAll()
//除以上的请求之外,都需要token
.anyRequest().authenticated())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
//配置formLogin
.formLogin(Customizer.withDefaults())
//将oauth2.0的配置托管给 SpringSecurity
.apply(authorizationServerConfigurer);
// 设置accesstoken为jwt形式
http.setSharedObject(OAuth2TokenCustomizer.class, oAuth2TokenCustomizer);
// 配置 异常处理
http
.exceptionHandling()
//当未登录的情况下 该如何跳转。
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint());
return http.build();
}
/**
* 根据数据库中的client信息转换
*
* @param clientId clientId
* @param oauthClient 数据库client
* @return RegisteredClient
*/
private RegisteredClient getRegisteredClient(String clientId, OauthClient oauthClient) {
RegisteredClient.Builder builder = RegisteredClient.withId(clientId)
.clientId(oauthClient.getClientId())
.clientSecret(oauthClient.getClientSecret())
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.redirectUri(oauthClient.getRedirectUri())
// JWT的配置项 包括TTL 是否复用refreshToken等等
.clientSettings(ClientSettings.builder()
//是否需要用户确认一下客户端需要获取用户的哪些权限
.requireAuthorizationConsent(false)
.build())
.tokenSettings(TokenSettings.builder()
//配置使用自定义的jwtToken格式化,配置此处才会使用到 CustomizerOAuth2Token , 或者不配置此格式化的配置,将默认生成jwt的形式
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
//是否可重用刷新令牌
.reuseRefreshTokens(true)
//accessToken 的有效期 单位:秒
.accessTokenTimeToLive(Duration.of(oauthClient.getAccessTokenTimeout(), ChronoUnit.SECONDS))
//refreshToken 的有效期 单位:秒
.refreshTokenTimeToLive(Duration.of(oauthClient.getRefreshTokenTimeout(), ChronoUnit.SECONDS))
.build());
//批量设置当前的授权类型
Arrays.stream(oauthClient.getGrantTypes().split(StrPool.COMMA))
.map(grantType -> {
if (CharSequenceUtil.equals(grantType, AuthorizationGrantType.AUTHORIZATION_CODE.getValue())) {
return AuthorizationGrantType.AUTHORIZATION_CODE;
} else if (CharSequenceUtil.equals(grantType, AuthorizationGrantType.REFRESH_TOKEN.getValue())) {
return AuthorizationGrantType.REFRESH_TOKEN;
} else if (CharSequenceUtil.equals(grantType, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())) {
return AuthorizationGrantType.CLIENT_CREDENTIALS;
} else if (CharSequenceUtil.equals(grantType, AuthorizationGrantType.PASSWORD.getValue())) {
return AuthorizationGrantType.PASSWORD;
} else if (CharSequenceUtil.equals(grantType, AuthorizationGrantType.JWT_BEARER.getValue())) {
return AuthorizationGrantType.JWT_BEARER;
} else {
throw new RegClientException("不支持的授权模式, [" + grantType + "]");
}
}).forEach(builder::authorizationGrantType);
Arrays.stream(oauthClient.getScopes().split(StrPool.COMMA))
.forEach(builder::scope);
return builder.build();
}
/**
* 注册client
*
* @param clientService 自定义的client端信息
* @return RegisteredClientRepository
*/
@Bean
public RegisteredClientRepository registeredClientRepository(OauthClientService clientService) {
return new RegisteredClientService(clientService);
}
在此处将不再赘述。可以在文章结尾查看源码。
对于access_token,需要哪些信息,将掌控到什么程度,在这里具体的体现,由开发人员指定,为系统中更敏捷的开发做准备。
/**
* JWT的加密算法,说明:https://www.rfc-editor.org/rfc/rfc7515
*
* @return JWKSource
*/
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
/**
* 将重写jwtToken的中的信息,并将其存储到redis中。
*
* @param context JwtEncodingContext
*/
@Override
public void customize(JwtEncodingContext context) {
//此处的token字符串是前端拿到的jwtToken信息中解密后的字符串,在这里将自定义jwtToken的实现,将定制jwt的 header 和 claims,将此token存放到 claim 中
String token = UUID.randomUUID().toString(true);
Authentication principal = context.getPrincipal();
Authentication authorizationGrant = context.getAuthorizationGrant();
OAuth2Authorization authorization = context.getAuthorization();
Set<String> authorizedScopes = context.getAuthorizedScopes();
ProviderContext providerContext = context.getProviderContext();
RegisteredClient registeredClient = context.getRegisteredClient();
log.info("principal-{}", JSONUtil.toJsonStr(principal));
log.info("authorization-{}", JSONUtil.toJsonStr(authorization));
log.info("authorizedScopes-{}", JSONUtil.toJsonStr(authorizedScopes));
log.info("authorizationGrant-{}", JSONUtil.toJsonStr(authorizationGrant));
log.info("providerContext-{}", JSONUtil.toJsonStr(providerContext));
log.info("registeredClient-{}", JSONUtil.toJsonStr(registeredClient));
UserDetail userDetail = null;
// 目的是为了定制jwt 的header 和 claims
if (principal instanceof OAuth2ClientAuthenticationToken) {
//如果当前登录的是client,则进行封装client
// userDetail = securityAuthUserService.createUserDetailByClientId(registeredClient.getClientId());
}
// else if (principal.getPrincipal() instanceof UserDetail) {
// //如果当前登录的是系统用户,则进行封装userDetail
// userDetail = securityAuthUserService.createUserDetailByUser((UserDetails) principal.getPrincipal());
// }
else if (principal.getPrincipal() instanceof User) {
//如果当前登录的是系统用户,则进行封装userDetail
userDetail = securityAuthUserService.createUserDetailByUser((User) principal.getPrincipal());
}
//如果解析失败,则抛出异常信息。
if (Objects.isNull(userDetail)) {
log.error("在自定义token实现中, 用户信息解析异常。");
userDetail = new UserDetail();
}
//也需要将此token存放到当前登录用户中,为了在退出登录时进行获取redis中的信息并将其删除
userDetail.setToken(token);
//将用户信息放置到redis中,并设置其过期时间为 client中的过期时间
strRedisHelper.strSet(LifeSecurityConstants.getUserTokenKey(token), userDetail,
registeredClient.getTokenSettings().getAccessTokenTimeToLive().getSeconds(), TimeUnit.SECONDS);
log.info("生成的用户-token是-{},此token作为key,用户信息作为value存储到redis中", token);
//也可以在此处将当前登录用户的信息存放到jwt中,但是这样就不再安全。
context.getClaims().claim(LifeSecurityConstants.TOKEN, token).build();
}
重写 logout 方法,对 当前登录用户中的token进行删除,并返回 退出登录成功提示。
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
log.info("进入退出登录处理器。");
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
try {
UserDetail userDetail = UserHelper.getUserDetail();
log.debug("当前登录用户-UserDetail-是:" + userDetail);
if (Objects.nonNull(userDetail)) {
String userToken = userDetail.getToken();
log.debug("当前登录用户的token-是:" + userToken);
String cacheUserToken = KvpFormat.of(SecurityConstant.USER_DETAIL).add("token", userToken).format();
redisHelper.delete(cacheUserToken);
redisHelper.delete(KvpFormat.of(SecurityConstant.TOKEN_STORE).add("userId", userDetail.getUserId().toString()).format());
}
SecurityContextHolder.clearContext();
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
log.debug("请求头-Authorization-是:" + token);
if (StrUtil.isBlank(token)) {
PrintWriter out = response.getWriter();
out.write(JSONUtil.toJsonStr(new Res<>(Res.ERROR, "未找到token,请确认已登录。", null)));
out.flush();
out.close();
}
token = token.split(" ")[1];
OAuth2Authorization auth2Authorization = oAuth2AuthorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
log.debug("查询出来-OAuth2Authorization-是:" + JSONUtil.toJsonStr(auth2Authorization));
if (Objects.nonNull(auth2Authorization)) {
oAuth2AuthorizationService.remove(auth2Authorization);
}
PrintWriter out = response.getWriter();
out.write(JSONUtil.toJsonStr(new Res<>(Res.SUCCESS, Res.SUCCESS_DESC, null)));
out.flush();
out.close();
} catch (IOException e) {
log.error("退出登录处理器处理失败,", e);
}
}
此处的配置将指定了 springsecurity中的自定义登录页面,对于 RouterFunction 可自行网上查询。
对于前后端分离中如何指定登录页面,如何跳转到登录页面,这个有很长一段时间由于知识的不足而不知如何处理,此配置将解决此问题,nginx中配置 / 代理 gwteway网关工程, 当访问根目录时将重定向到 /login 路径上,nginx中对 /login 路径做代理到 前端工程中, 其 /login 所指向的前端工程就是 自定义的登录页面。
@Slf4j
@Configuration
public class RouterFunctionConfig {
/**
* 访问根目录时将重定向到登录模块
*
* @return 登录模块
*/
@Bean
public RouterFunction<ServerResponse> loginRouterFunction() {
return RouterFunctions.route(
RequestPredicates.GET("/"),
request -> ServerResponse.temporaryRedirect(URI.create(request.uri() + "login")).build());
}
}
将解析 /oauth/token 接口中返回的accessToken信息,获取其中的 token字段对应的redis存储的用户信息,并验证是否能解析,在转换为用户信息的之后,将对请求路径做校验,判断是否拥有此权限。
此处是gateway中做的accessToken解析,但是路由后的服务可能是不知道的,这个时候,我们需要对此用户信息再次做一层转换,转换为Jwt形式的token,在公共的core模块中对此token进行解析,并放置到 SecurityContextHolder.getContext()中。
// JwtTokenGatewayFilterFactory
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String token = getToken(request);
// 获取当前用户的信息
String userDetailStr = StrUtil.isBlank(token) ? null : redisHelper.strGet(LifeSecurityConstants.getUserTokenKey(token));
// 若jwt不存在,则封入一个空字符串,到权限拦截器处理。因为有些api是不需要登录的,故在此不处理。
UserDetail userDetail = StrUtil.isNotBlank(userDetailStr) ? JsonHelper.readValue(userDetailStr, UserDetail.class) : null;
userDetailStr = Optional.ofNullable(userDetailStr).orElse(StrUtil.EMPTY);
// 创建JWS对象
JWSObject jwsObject = new JWSObject(jwsHeader, new Payload(userDetailStr));
// 签名并序列化转换为真正存储用户信息的jwtToken
String jwtToken = Jwts.signAndSerialize(jwsObject, signer);
ServerWebExchange jwtExchange = exchange.mutate()
.request(request.mutate()
.header(Jwts.HEADER_JWT, jwtToken).build())
.build();
return chain.filter(jwtExchange)
.contextWrite(ctx -> ctx.put(RequestContext.CTX_KEY,
ctx.<RequestContext>getOrEmpty(RequestContext.CTX_KEY)
.orElse(new RequestContext())
.setUserDetail(userDetail)));
};
}
// JwtAuthenticationFilter
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
log.info("authentication-{}",JSONUtil.toJsonStr(authentication));
log.info("请求路径是-{}", JSONUtil.toJsonStr(request.getRequestURI()));
String jwt = request.getHeader(Jwts.HEADER_JWT);
log.info("进入到-JwtAuthenticationFilter-过滤器-jwtToken-{}", jwt);
if (StrUtil.isBlank(jwt)) {
chain.doFilter(request, response);
return;
}
JWSObject jwsObject = Jwts.parse(jwt);
if (!Jwts.verify(jwsObject, verifier)) {
log.error("Jwt verify failed! JWT: [{}]", jwt);
chain.doFilter(request, response);
return;
}
UserDetail userDetail = jwsObject.getPayload().toType(payload -> StrUtil.isBlank(payload.toString()) ?
UserDetail.anonymous() : JsonHelper.readValue(payload.toString(), UserDetail.class));
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetail, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request, response);
}
解决参考:
https://blog.csdn.net/qq27229639/article/details/103069055
https://www.ancii.com/anbgjpemb
需要放置到一个 单独的 @Configuration 配置类中,不然会不生效的。