微服务整合Spring Security6和使用Authorization Server进行授权与验证
Spring Security官方文档 New Authorization Server官方文档
Spring Security6针对 Spring Security5进行了大量变更,并且原本的 Authorization Server服务停止更新后,Spring针对新的 Spring Security6推出了新的基于 OAuth2.1协议的 Authorization Server。
写文章的起因就是在做微服务项目的时候需要使用 Spring Security的 OAuth2进行认证和授权,结果发现网上基本都是基于 Spring Security5的文章。就算有基于 Spring Security6的,也就是把官方的教程稍微一下就发出来了,基本没有参考价值。
本文简单的说一下 Spring Security6的使用和基于 Authorization Server的 OAuth2协议进行认证和授权。还有就是实现在 OAuth2.1协议中被废弃的 Password验证方式。
准备工作
Java 版本
17Spring Boot 版本
3.0.2Spring Cloud 版本
2022.0.0Spring Security Authorization Server 版本
1.0.3此版本还没有官方的自动配置,需要使用配置类配置,在下一个版本中可以使用配置文件配置
添加依赖
<!-- authorization-server 自动依赖与 spring-security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
</dependency>
<!-- 数据库中查询用户 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
实现简单登录保护
编写 SecurityConfig配置类
// 开启 Security
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class XueChengSecurityConfig {
@Bean
@ConditionalOnMissingBean(UserDetailsService.class)
// 基于内存的用户管理 生成两个用户 角色分别是user和admin
public UserDetailsService userDetailsService() {
UserDetails user1 = User
.withUsername("user")
.password("123456")
.roles("user")
.build();
UserDetails user2 = User
.withUsername("admin")
.password("123456")
.roles("admin")
.build();
return new InMemoryUserDetailsManager(user1, user2);
}
@Bean
// 配置Security拦截路径
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
// 指定路径需要指定角色的用户登录后才能访问
.requestMatchers("/role/r1").hasRole("user")
.requestMatchers("/role/r2").hasRole("admin")
// 放行所有 /oauth2 开头的请求
.requestMatchers("/oauth2/**").permitAll()
// 其余请求都需要登录才能访问
.anyRequest().authenticated())
// 关闭csrf
.csrf().disable()
// 基于Form表单认证的方式,使用默认配置
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
// 密码明文验证
return return NoOpPasswordEncoder.getInstance();
}
}
创建测试 Controller
@RestController
@Tag(name = "登录接口")
public class LoginController {
private final UserMapper userMapper;
private LoginController(UserMapper userMapper) {
this.userMapper = userMapper;
}
@RequestMapping("/success")
public String loginSuccess() {
return "登陆成功";
}
@GetMapping("/user/{id}")
// 此注解作用和配置类中的 hasRole 一致
@PreAuthorize("hasRole('admin')")
public User getUser(@PathVariable("id") String id) {
return userMapper.selectById(id);
}
@GetMapping("/role/r1")
public String testRoleR1() {
return "r1";
}
@GetMapping("/role/r2")
public String testRoleR2() {
return "r2";
}
}
启动项目,访问 Controller类中的任意路径,会自动跳转到登录页面,登录成功后会跳转到登录成功页面。然后可以根据登录用户的角色访问对应的接口。
配置Authorization Server
由于 OAuth2.1协议启用了 Password方式认证,为了在项目中使用,需要自定义 Password方式的实现。
通过翻看源码,需要三个工具类(官方包中的,只不过方法仅包中可用,拷贝出来修改为 public)
OAuth2ConfigurerUtils.java
final class OAuth2ConfigurerUtils {
private OAuth2ConfigurerUtils() {
}
static RegisteredClientRepository getRegisteredClientRepository(HttpSecurity httpSecurity) {
RegisteredClientRepository registeredClientRepository = (RegisteredClientRepository)httpSecurity.getSharedObject(RegisteredClientRepository.class);
if (registeredClientRepository == null) {
registeredClientRepository = (RegisteredClientRepository)getBean(httpSecurity, RegisteredClientRepository.class);
httpSecurity.setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
}
return registeredClientRepository;
}
static OAuth2AuthorizationService getAuthorizationService(HttpSecurity httpSecurity) {
OAuth2AuthorizationService authorizationService = (OAuth2AuthorizationService)httpSecurity.getSharedObject(OAuth2AuthorizationService.class);
if (authorizationService == null) {
authorizationService = (OAuth2AuthorizationService)getOptionalBean(httpSecurity, OAuth2AuthorizationService.class);
if (authorizationService == null) {
authorizationService = new InMemoryOAuth2AuthorizationService();
}
httpSecurity.setSharedObject(OAuth2AuthorizationService.class, authorizationService);
}
return (OAuth2AuthorizationService)authorizationService;
}
static OAuth2AuthorizationConsentService getAuthorizationConsentService(HttpSecurity httpSecurity) {
OAuth2AuthorizationConsentService authorizationConsentService = (OAuth2AuthorizationConsentService)httpSecurity.getSharedObject(OAuth2AuthorizationConsentService.class);
if (authorizationConsentService == null) {
authorizationConsentService = (OAuth2AuthorizationConsentService)getOptionalBean(httpSecurity, OAuth2AuthorizationConsentService.class);
if (authorizationConsentService == null) {
authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService();
}
httpSecurity.setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
}
return (OAuth2AuthorizationConsentService)authorizationConsentService;
}
static OAuth2TokenGenerator<? extends OAuth2Token> getTokenGenerator(HttpSecurity httpSecurity) {
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator = (OAuth2TokenGenerator)httpSecurity.getSharedObject(OAuth2TokenGenerator.class);
if (tokenGenerator == null) {
tokenGenerator = (OAuth2TokenGenerator)getOptionalBean(httpSecurity, OAuth2TokenGenerator.class);
if (tokenGenerator == null) {
JwtGenerator jwtGenerator = getJwtGenerator(httpSecurity);
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer = getAccessTokenCustomizer(httpSecurity);
if (accessTokenCustomizer != null) {
accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer);
}
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
if (jwtGenerator != null) {
tokenGenerator = new DelegatingOAuth2TokenGenerator(new OAuth2TokenGenerator[]{jwtGenerator, accessTokenGenerator, refreshTokenGenerator});
} else {
tokenGenerator = new DelegatingOAuth2TokenGenerator(new OAuth2TokenGenerator[]{accessTokenGenerator, refreshTokenGenerator});
}
}
httpSecurity.setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);
}
return (OAuth2TokenGenerator)tokenGenerator;
}
private static JwtGenerator getJwtGenerator(HttpSecurity httpSecurity) {
JwtGenerator jwtGenerator = (JwtGenerator)httpSecurity.getSharedObject(JwtGenerator.class);
if (jwtGenerator == null) {
JwtEncoder jwtEncoder = getJwtEncoder(httpSecurity);
if (jwtEncoder != null) {
jwtGenerator = new JwtGenerator(jwtEncoder);
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = getJwtCustomizer(httpSecurity);
if (jwtCustomizer != null) {
jwtGenerator.setJwtCustomizer(jwtCustomizer);
}
httpSecurity.setSharedObject(JwtGenerator.class, jwtGenerator);
}
}
return jwtGenerator;
}
private static JwtEncoder getJwtEncoder(HttpSecurity httpSecurity) {
JwtEncoder jwtEncoder = (JwtEncoder)httpSecurity.getSharedObject(JwtEncoder.class);
if (jwtEncoder == null) {
jwtEncoder = (JwtEncoder)getOptionalBean(httpSecurity, JwtEncoder.class);
if (jwtEncoder == null) {
JWKSource<SecurityContext> jwkSource = getJwkSource(httpSecurity);
if (jwkSource != null) {
jwtEncoder = new NimbusJwtEncoder(jwkSource);
}
}
if (jwtEncoder != null) {
httpSecurity.setSharedObject(JwtEncoder.class, jwtEncoder);
}
}
return (JwtEncoder)jwtEncoder;
}
static JWKSource<SecurityContext> getJwkSource(HttpSecurity httpSecurity) {
JWKSource<SecurityContext> jwkSource = (JWKSource)httpSecurity.getSharedObject(JWKSource.class);
if (jwkSource == null) {
ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, new Class[]{SecurityContext.class});
jwkSource = (JWKSource)getOptionalBean(httpSecurity, type);
if (jwkSource != null) {
httpSecurity.setSharedObject(JWKSource.class, jwkSource);
}
}
return jwkSource;
}
private static OAuth2TokenCustomizer<JwtEncodingContext> getJwtCustomizer(HttpSecurity httpSecurity) {
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, new Class[]{JwtEncodingContext.class});
return (OAuth2TokenCustomizer)getOptionalBean(httpSecurity, type);
}
private static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> getAccessTokenCustomizer(HttpSecurity httpSecurity) {
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, new Class[]{OAuth2TokenClaimsContext.class});
return (OAuth2TokenCustomizer)getOptionalBean(httpSecurity, type);
}
static AuthorizationServerSettings getAuthorizationServerSettings(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = (AuthorizationServerSettings)httpSecurity.getSharedObject(AuthorizationServerSettings.class);
if (authorizationServerSettings == null) {
authorizationServerSettings = (AuthorizationServerSettings)getBean(httpSecurity, AuthorizationServerSettings.class);
httpSecurity.setSharedObject(AuthorizationServerSettings.class, authorizationServerSettings);
}
return authorizationServerSettings;
}
static <T> T getBean(HttpSecurity httpSecurity, Class<T> type) {
return ((ApplicationContext)httpSecurity.getSharedObject(ApplicationContext.class)).getBean(type);
}
static <T> T getBean(HttpSecurity httpSecurity, ResolvableType type) {
ApplicationContext context = (ApplicationContext)httpSecurity.getSharedObject(ApplicationContext.class);
String[] names = context.getBeanNamesForType(type);
if (names.length == 1) {
return (T) context.getBean(names[0]);
} else if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
} else {
throw new NoSuchBeanDefinitionException(type);
}
}
static <T> T getOptionalBean(HttpSecurity httpSecurity, Class<T> type) {
Map<String, T> beansMap = BeanFactoryUtils.beansOfTypeIncludingAncestors((ListableBeanFactory)httpSecurity.getSharedObject(ApplicationContext.class), type);
if (beansMap.size() > 1) {
int var10003 = beansMap.size();
String var10004 = type.getName();
throw new NoUniqueBeanDefinitionException(type, var10003, "Expected single matching bean of type '" + var10004 + "' but found " + beansMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(beansMap.keySet()));
} else {
return !beansMap.isEmpty() ? beansMap.values().iterator().next() : null;
}
}
static <T> T getOptionalBean(HttpSecurity httpSecurity, ResolvableType type) {
ApplicationContext context = (ApplicationContext)httpSecurity.getSharedObject(ApplicationContext.class);
String[] names = context.getBeanNamesForType(type);
if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
} else {
return names.length == 1 ? (T) context.getBean(names[0]) : null;
}
}
}
OAuth2AuthenticationProviderUtils.java
public final class OAuth2AuthenticationProviderUtils {
public static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
OAuth2ClientAuthenticationToken authenticationToken = null;
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getClass())) {
authenticationToken = (OAuth2ClientAuthenticationToken) authentication;
}
if (authenticationToken != null && authenticationToken.isAuthenticated()) {
return authenticationToken;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
}
OAuth2EndpointUtils.java
public final class OAuth2EndpointUtils {
public static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
for (String value : values) {
parameters.add(key, value);
}
});
return parameters;
}
public static void throwError(String errorCode, String parameterName, String errorUri) {
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
throw new OAuth2AuthenticationException(error);
}
}
从这里开始对官方的Converter和Provider进行自定义实现,以使用Password认证
添加 Password认证的 OAuth2PasswordGrantAuthenticationToken类
@Getter
public class OAuth2PasswordGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
private final String username;
private final String password;
private final String clientId;
private final Set<String> scopes;
public OAuth2PasswordGrantAuthenticationToken(String username, String password, Authentication authentication, Set<String> scopes) {
super(OAuth2PasswordGrantAuthenticationConverter.PASSWORD_GRANT_TYPE, authentication, null);
this.username = username;
this.password = password;
this.clientId = authentication.getName();
this.scopes = scopes;
}
}
实现自定义的 AuthenticationConverter
public class OAuth2PasswordGrantAuthenticationConverter implements AuthenticationConverter {
// 创建一个新的AuthorizationGrantType
protected static final AuthorizationGrantType PASSWORD_GRANT_TYPE = new AuthorizationGrantType("password");
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
@Override
public Authentication convert(HttpServletRequest request) {
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!PASSWORD_GRANT_TYPE.getValue().equals(grantType)) {
return null;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
OAuth2EndpointUtils.throwError("invalid_request", OAuth2ParameterNames.CLIENT_ID, ERROR_URI);
}
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// 验证客户端ID
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
if (!StringUtils.hasText(clientId) || parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1 ) {
OAuth2EndpointUtils.throwError("invalid_request", OAuth2ParameterNames.CLIENT_ID, ERROR_URI);
}
// 判断用户名是否存在
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
OAuth2EndpointUtils.throwError("invalid_request", OAuth2ParameterNames.USERNAME, ERROR_URI);
}
// 判断密码是否存在
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
OAuth2EndpointUtils.throwError("invalid_request", OAuth2ParameterNames.PASSWORD, ERROR_URI);
}
// 获取权限字段
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
Set<String> scopes = scope != null ? Set.of(scope.split(" ")) : null;
return new OAuth2PasswordGrantAuthenticationToken(username, password, authentication, scopes);
}
}
实现自定义的 AuthenticationProvider
public class OAuth2PasswordGrantAuthenticationProvider implements AuthenticationProvider {
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType("id_token");
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
public OAuth2PasswordGrantAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 官方扩展教材的写法
OAuth2PasswordGrantAuthenticationToken passwordAuthenticationToken = (OAuth2PasswordGrantAuthenticationToken) authentication;
OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient((Authentication) passwordAuthenticationToken.getPrincipal());
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
if (registeredClient == null || !registeredClient.getAuthorizationGrantTypes().contains(passwordAuthenticationToken.getGrantType())) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
Set<String> scopes = passwordAuthenticationToken.getScopes();
// 如果请求token时的权限字段为空,那么给予所有权限
if (scopes != null && !scopes.isEmpty()) {
for (String scope : scopes) {
Set<String> registeredScopes = registeredClient.getScopes();
if (!registeredScopes.contains(scope)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
}
}else {
scopes = registeredClient.getScopes();
}
// 拿到用户名密码 然后从userDetailsService中根据用户名获取用户
String username = passwordAuthenticationToken.getUsername();
String password = passwordAuthenticationToken.getPassword();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 判断密码是否正确
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new OAuth2AuthenticationException("The resource does not exist or the credentials are invalid");
}
// 默认生成Token的Builder类
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(clientPrincipal)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(scopes)
.authorizationGrantType(passwordAuthenticationToken.getGrantType())
.authorizationGrant(passwordAuthenticationToken);
// 生成AccessToken
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the access token.", null);
throw new OAuth2AuthenticationException(error);
}
// 往AccessToken中添加用户信息
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, generatedAccessToken.getTokenValue(),
generatedAccessToken.getIssuedAt(), generatedAccessToken.getExpiresAt(), scopes);
Map<String, Object> tokenData = new HashMap<>();
tokenData.put("username", userDetails.getUsername());
tokenData.put("roles", userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()));
if (!scopes.isEmpty()) {
tokenData.put("scopes", scopes);
}
OAuth2Authorization.Builder builder = OAuth2Authorization
.withRegisteredClient(registeredClient)
.principalName(userDetails.getUsername())
.authorizationGrantType(passwordAuthenticationToken.getGrantType())
.token(accessToken, m -> m.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, tokenData));
// 生成 refreshToken
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getTokenSettings().isReuseRefreshTokens()) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the refresh token.", null);
throw new OAuth2AuthenticationException(error);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
builder.refreshToken(refreshToken);
}
Map<String, Object> additionalParameters = Collections.emptyMap();
// 如果要求的权限包括 openid那么生成IdToken 以通过 /userinfo 接口获取用户信息
if (scopes.contains("openid")) {
tokenContext = tokenContextBuilder
.tokenType(ID_TOKEN_TOKEN_TYPE)
.authorization(builder.build())
.build();
OAuth2Token generatedIdToken = tokenGenerator.generate(tokenContext);
if (!(generatedIdToken instanceof Jwt jwtToken)) {
OAuth2Error error = new OAuth2Error("server_error", "The token generator failed to generate the ID token.", "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2");
throw new OAuth2AuthenticationException(error);
}
OidcIdToken idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedAccessToken.getIssuedAt(), generatedAccessToken.getExpiresAt(), jwtToken.getClaims());
builder.token(idToken, m -> m.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
additionalParameters = new HashMap<>();
additionalParameters.put("id_token", idToken.getTokenValue());
}
// 根据生成Token的不同 返回不同的返回结果
OAuth2AccessTokenAuthenticationToken authenticationToken = null;
if (refreshToken != null && !additionalParameters.isEmpty()) {
authenticationToken = new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
}else if (refreshToken != null) {
authenticationToken = new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken);
}else {
authenticationToken = new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken);
}
authorizationService.save(builder.build());
return authenticationToken;
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2PasswordGrantAuthenticationToken.class.isAssignableFrom(authentication);
}
}
编写 Authorization Server配置类
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) throws Exception {
// 固定写法
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator = OAuth2ConfigurerUtils.getTokenGenerator(http);
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults())
// 添加自定义的Password验证器
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
.accessTokenRequestConverter(new OAuth2PasswordGrantAuthenticationConverter())
.authenticationProvider(new OAuth2PasswordGrantAuthenticationProvider(userDetailsService, passwordEncoder, authorizationService, objectMapper, tokenGenerator, applicationContext)));
http.exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))
// ResourceServer验证格式使用jwt
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
@Bean
// 配置客户端
public RegisteredClientRepository registeredClientRepository(PasswordEncoder encoder) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
// 客户端ID和客户端机密 请求授权码和Token时需要
.clientId("xuecheng-web-app")
.clientSecret("xuecheng-web-app")
// 客户端ID和机密使用POST表单验证的方式
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
// 支持的GrantType格式,支持授权吗、刷新令牌和密码三种方式
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(new AuthorizationGrantType("password"))
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.scope(OidcScopes.OPENID)
.scope("all")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
// 获取授权码后的重定向链接
.redirectUri("http://www.xuecheng.ml")
// Token有效期设置
.tokenSettings(TokenSettings.builder()
// accessToekn有效时间 3小时
.accessTokenTimeToLive(Duration.ofHours(3))
// refreshToken有效时间 3天
.refreshTokenTimeToLive(Duration.ofDays(3))
// 开启 refreshToken 支持
.reuseRefreshTokens(true)
.build())
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
@Bean
// JWT的加密策略
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
@Bean
// Jwt的解密类
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
// 默认的AuthorizationServerSettings
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
验证是否可用
- 获取Token:
http://localhost:8074/auth/oauth2/token?client_id=xuecheng-web-app&client_secret=xuecheng-web-app&grant_type=password&username=user&password=123456&redirect_uri=http://www.xuecheng.ml。(Password模式,替换为实际的值, 如果返回Token即成功。其他模式参考官方文档) - 验证Token:
http://localhost:8074/auth/oauth2/introspect?token=eyJraWQiOiJlYzllNzVmNy0yZTVmLTQwYTgtOGZjZS00N2E3MjUzZTA2N2YiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ4dWVjaGVuZy13ZWItYXBwIiwiYXVkIjoieHVlY2hlbmctd2ViLWFwcCIsIm5iZiI6MTY5NjM0MTM0OCwic2NvcGUiOlsiYWxsIiwib3BlbmlkIl0sImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA3NC9hdXRoIiwiZXhwIjoxNjk2MzUyMTQ4LCJpYXQiOjE2OTYzNDEzNDh9.Ew10W8K0mV0eTs7LmvhlCkrgt5XD4V346ZfMGGCcyifC5ZARF2RMZzAfs_8og4XBNiHzBHuB_VdLNJowtH-UnPeE2Oc_hRA_gIwT8AaSJJhfwyrOzKyDXTYhwmmbhb1RHj8oUn0MGdu3dlECmhPzYzJg1C29lJp8FD3Cets_rgBMU0nRVDmpDhgjBzinTtOX50yztqesKndITob7TKFrT4P2QBS2-FwtEDw3O_Y1jWJKDWRmWvSsBVZ6ruCIST6ypjmXbpF6A2a0ESUXoRYpNZ7T4tC_98ux912w_0HqowTzr3BF4nhOWTTe5cEc36Okb_eFGdKWzf-Z4KXeqFLuVg&client_id=xuecheng-web-app&client_secret=xuecheng-web-app。(会返回用户信息、权限、Token有效时间等信息,将Token替换为返回的accessToekn) - 获取用户信息:
http://localhost:8074/auth/userinfo。(需要添加Bearer验证请求头,参数为返回的accessToekn)
实现从数据库中查询用户
上面例子中的用户都是在配置类中创建的基于内存的方式保存,在实际项目中用户往往需要从数据库中读取。可以实现 Spring Security的 UserDetailsService接口然后将实现类注入到 IOC容器即可
@Service
public class UserServiceImpl implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
private final UserMapper userMapper;
public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
// 通过用户名查询用户
queryWrapper.eq(User::getUsername, username);
User findUser = userMapper.selectOne(queryWrapper);
if (findUser == null) {
logger.info("查询用户为空,用户名:{}", username);
return null;
}
// 返回Spring security中的User对象
return new org.springframework.security.core.userdetails.User(findUser.getUsername(), findUser.getPassword(),
List.of(new SimpleGrantedAuthority("user")));
}
}
上面的例子可以实现从数据库中查询用户,但是如果用户登陆成功,那么通过获取用户信息接口请求得到的只有用户名。而我们往往需要将用户的其它信息也一起暴露出去,这里演示一个简单的实现方法,不需要实现 UserDetails
@Service
public class UserServiceImpl implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
private final UserMapper userMapper;
private final ObjectMapper objectMapper;
public UserServiceImpl(UserMapper userMapper, ObjectMapper objectMapper) {
this.userMapper = userMapper;
this.objectMapper = objectMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername, username);
User findUser = userMapper.selectOne(queryWrapper);
if (findUser == null) {
logger.info("查询用户为空,用户名:{}", username);
return null;
}
String password = findUser.getPassword();
findUser.setPassword(null);
try{
// 直接将用户的所有信息通过Json序列化后存放在username字段,前端拿到后发序列化即可
String jsonUser = objectMapper.writeValueAsString(findUser);
return new org.springframework.security.core.userdetails.User(jsonUser, password,
List.of(new SimpleGrantedAuthority("user")));
}catch (JsonProcessingException e) {
logger.error("序列化用户信息失败,错误消息:{}", e.getMessage());
}
return null;
}
}
由于数据库中的密码是加密的,所有之前注入的明文密码验证器不能使用了,需要注入新的验证器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
网关统一验证
在微服务项目中,往往由网关对客户端的登录/认证状态进行验证,然后再有具体的微服务进行角色或权限的验证。
这里主要说一下如何在网关中通过 OAuth2请求 Authorization Server认证服务器对客户端进行统一验证
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- OAuth2 资源服务器 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<!-- JWt加解密 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
更改配置文件,添加认证服务器地址
spring:
security:
oauth2:
resourceserver:
jwt:
# 认证服务器的微服务地址
issuer-uri: http://localhost:8074/auth
添加请求验证配置类
@Configuration
public class GatewaySecurityConfig {
@Bean
SecurityWebFilterChain gatewayExchangeSecurityFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange(exchanges -> exchanges
// 需要放行的请求
.pathMatchers("/auth/**", "/content/open/**", "/media/open/**").permitAll()
.anyExchange().authenticated())
.csrf().disable()
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt).build();
}
}
做完上面这些后,当请求再次进入网关时,如果没有携带由认证服务器颁发的Token,那么会报 401错误
在方法中获取用户信息
在上面已经把所有的用户信息都包含在了 username参数中,如果需要获取用户信息的话只需要在 Controller方法中添加一个注解和参数即可
@RestController
@EnableMethodSecurity
public class DemoController {
@GetMapping("/hello")
// 通过AuthenticationPrincipal注解拿到Jwt中的参数
public String hello(@AuthenticationPrincipal Jwt jwt) {
// 拿到用户名之后进行反序列化即可
String subject = jwt.getSubject();
return "hello";
}
}






0 条评论