新项目都开始用JDK17了,Springboot也用3.x了,对应的认证授权服务也要升级了。Spring OAuth2.0不再支持了,开始使用Spring Authorization Server了。因为去掉了密码模式,而我们项目最适用的还是密码模式,这里我们使用OAuth2.1的扩展功能补上密码模式。
参考官方文档,给出了步骤:
此次使用Springboot版本为:3.3.3
依赖引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>
1. 密码模式
按照文档,添加密码模式,主要添加几个文件:
- Converter类:主要是限定请求Token的参数的,并未验证参数的值,验证好参数后,会将参数封装成passwordToken实体,往下传
- Provider类,主要作用2个,1是验证参数值,2是用这些数据生成Token。数据来源就是Converter类传过来的passwordToken实体
- passwordToken实体:实体类功能,有一些必须得属性
- 密码模式用到的工具类util
以上内容都是参考授权码模式或是客户端模式来写的。
1.1 Converter类 - OAuth2PasswordAuthenticationConverter
/**
* 限定必须要传的参数
* 只是要这些参数,并没有对参数值校验
*/
public class OAuth2PasswordAuthenticationConverter implements AuthenticationConverter {
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
// 1. 提取表单参数,准备校验用,用的这个方法就是复制的sas自带的
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getFormParameters(request);
// 2. 授权类型参数 (必须)
// String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
String grantType = parameters.getFirst(OAuth2ParameterNames.GRANT_TYPE);
if (!OAuth2PasswordAuthenticationToken.PASSWORD.getValue().equals(grantType)) {
return null;
}
// 3. 令牌申请访问范围参数 (可选)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.SCOPE,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
// 4. 账号密码参数校验
// 4.1 用户名参数 (必须)
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.USERNAME,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// 4.2 密码参数 (必须)
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.PASSWORD,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// 5. 客户端凭据信息,在header 中填写的那个
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
// 6. 参数封装成additionalParameters放到OAuth2PasswordAuthenticationToken中传给 PasswordAuthenticationProvider 用于验证值
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) && !key.equals(OAuth2ParameterNames.SCOPE)) {
additionalParameters.put(key, value.get(0));
}
});
return new OAuth2PasswordAuthenticationToken(clientPrincipal, requestedScopes, additionalParameters);
}
}
1.2 Provider类 - OAuth2PasswordAuthenticationProvider
/**
* 参考授权码(主要)"OAuth2AuthorizationCodeAuthenticationProvider"和客户端"OAuth2ClientCredentialsAuthenticationProvider"
*/
public class OAuth2PasswordAuthenticationProvider implements AuthenticationProvider {
@Resource
private PasswordEncoder passwordEncoder;
// 这部分代码和OAuth2ClientCredentialsAuthenticationProvider类似,只是添加了AuthenticationManager
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private final AuthenticationManager authenticationManager;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
public OAuth2PasswordAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authenticationManager = authenticationManager;
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// PasswordAuthenticationToken
OAuth2PasswordAuthenticationToken passwordAuthenticationToken = (OAuth2PasswordAuthenticationToken) authentication;
// coverter中最后生成的token,包含3部分内容,在这里拿出来,下面用
Map<String, Object> additionalParameters = passwordAuthenticationToken.getAdditionalParameters();
OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient(passwordAuthenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// 1. 验证客户端是否支持密码模式类型(grant_type=password)
if (!registeredClient.getAuthorizationGrantTypes().contains(passwordAuthenticationToken.getGrantType())) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE);
}
// 2. 校验范围scope
Set<String> authorizedScopes = registeredClient.getScopes();
Set<String> requestedScopes = passwordAuthenticationToken.getScopes();
if (!CollectionUtils.isEmpty(requestedScopes )) {
Set<String> unauthorizedScopes = requestedScopes.stream()
.filter(scope -> !registeredClient.getScopes().contains(scope))
.collect(Collectors.toSet());
if (!CollectionUtils.isEmpty(unauthorizedScopes)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
}
authorizedScopes = new LinkedHashSet<>(requestedScopes);
}
// 3 用户名密码校验
String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);
String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);
// 我们不自己校验了,用oauth2的方法校验
// MyUserDetail userDetail = userDetailsService.loadUserByUsername(username);
// if (userDetail == null) {
// throw new OAuth2AuthenticationException("用户不存在!");
// }
// if (!passwordEncoder.matches(password, userDetail.getPassword())) {
// throw new OAuth2AuthenticationException("密码不正确!");
// }
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
Authentication usernamePasswordAuthentication = null;
try {
usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
} catch (AuthenticationException e) {
e.printStackTrace();
throw new OAuth2AuthenticationException("账号或密码错误");
}
// 处理userDetails的时候,我们没有添加权限信息,这拿不到数据
// Collection<? extends GrantedAuthority> authorities = usernamePasswordAuthentication.getAuthorities();
// for (GrantedAuthority authoriti : authorities) {
// }
// 4. 生成token
// 4.1 填充token需要的上下文数据,按照授权码模式来的
Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
// 身份验证成功的认证信息(用户名、权限等信息)
.principal(usernamePasswordAuthentication)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(authorizedScopes)
// 授权类型
.authorizationGrantType(passwordAuthenticationToken.getGrantType())
// 授权具体对象
.authorizationGrant(passwordAuthenticationToken)
;
// 4.2 生成访问令牌(Access Token)
DefaultOAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
// 4. 生成刷新令牌(Refresh Token)
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// Do not issue refresh token to public client
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the refresh token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
}
// 5. 组装数据入库
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(clientPrincipal.getName())
.authorizationGrantType(passwordAuthenticationToken.getGrantType())
.authorizedScopes(authorizedScopes)
.attribute(Principal.class.getName(), usernamePasswordAuthentication);
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
} else {
authorizationBuilder.accessToken(accessToken);
authorizationBuilder.refreshToken(refreshToken);
}
OAuth2Authorization authorization = authorizationBuilder.build();
// 入库
this.authorizationService.save(authorization);
additionalParameters = Collections.emptyMap();
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2PasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
1.3 passwordToken实体 - OAuth2PasswordAuthenticationToken
/**
* 这个token就是传递数据用的一个实体,想要什么数据可以写成变量
* 在converter生成时赋值,在provider中拿出来用
*/
public class OAuth2PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
private static final long serialVersionUID = -7029686994815546552L;
public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password");
private final Set<String> scopes;
/**
* 密码模式身份验证令牌
*
* @param clientPrincipal 客户端信息
* @param scopes 令牌申请访问范围
* @param additionalParameters 自定义额外参数(用户名和密码等)
*/
protected OAuth2PasswordAuthenticationToken(Authentication clientPrincipal, @Nullable Set<String> scopes,
@Nullable Map<String, Object> additionalParameters) {
super(PASSWORD, clientPrincipal, additionalParameters);
this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
}
/**
* 这个方法 父类中直接返回了空字符串,
* 我们可以根据自己的需要重写,可以返回密码
*/
@Override
public Object getCredentials() {
return this.getAdditionalParameters().get(OAuth2ParameterNames.PASSWORD);
}
public Set<String> getScopes() {
return this.scopes;
}
}
1.4 util类
有2个,
OAuth2AuthenticationProviderUtils
public class OAuth2AuthenticationProviderUtils {
private OAuth2AuthenticationProviderUtils() {
}
public static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
OAuth2ClientAuthenticationToken clientPrincipal = null;
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
}
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
return clientPrincipal;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
public static <T extends OAuth2Token> OAuth2Authorization invalidate(OAuth2Authorization authorization, T token) {
// @formatter:off
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization)
.token(token,
(metadata) ->
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
if (OAuth2RefreshToken.class.isAssignableFrom(token.getClass())) {
authorizationBuilder.token(
authorization.getAccessToken().getToken(),
(metadata) ->
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
authorization.getToken(OAuth2AuthorizationCode.class);
if (authorizationCode != null && !authorizationCode.isInvalidated()) {
authorizationBuilder.token(
authorizationCode.getToken(),
(metadata) ->
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
}
}
// @formatter:on
return authorizationBuilder.build();
}
public static <T extends OAuth2Token> OAuth2AccessToken accessToken(OAuth2Authorization.Builder builder, T token,
OAuth2TokenContext accessTokenContext) {
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token.getTokenValue(),
token.getIssuedAt(), token.getExpiresAt(), accessTokenContext.getAuthorizedScopes());
OAuth2TokenFormat accessTokenFormat = accessTokenContext.getRegisteredClient()
.getTokenSettings()
.getAccessTokenFormat();
builder.token(accessToken, (metadata) -> {
if (token instanceof ClaimAccessor claimAccessor) {
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, claimAccessor.getClaims());
}
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false);
metadata.put(OAuth2TokenFormat.class.getName(), accessTokenFormat.getValue());
});
return accessToken;
}
}
- OAuth2EndpointUtils
public class OAuth2EndpointUtils {
public static final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private OAuth2EndpointUtils() {
}
public static MultiValueMap<String, String> getFormParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameterMap.forEach((key, values) -> {
String queryString = StringUtils.hasText(request.getQueryString()) ? request.getQueryString() : "";
// If not query parameter then it's a form parameter
if (!queryString.contains(key) && values.length > 0) {
for (String value : values) {
parameters.add(key, value);
}
}
});
return parameters;
}
public static MultiValueMap<String, String> getQueryParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameterMap.forEach((key, values) -> {
String queryString = StringUtils.hasText(request.getQueryString()) ? request.getQueryString() : "";
if (queryString.contains(key) && values.length > 0) {
for (String value : values) {
parameters.add(key, value);
}
}
});
return parameters;
}
public static Map<String, Object> getParametersIfMatchesAuthorizationCodeGrantRequest(HttpServletRequest request,
String... exclusions) {
if (!matchesAuthorizationCodeGrantRequest(request)) {
return Collections.emptyMap();
}
MultiValueMap<String, String> multiValueParameters = "GET".equals(request.getMethod())
? getQueryParameters(request) : getFormParameters(request);
for (String exclusion : exclusions) {
multiValueParameters.remove(exclusion);
}
Map<String, Object> parameters = new HashMap<>();
multiValueParameters.forEach(
(key, value) -> parameters.put(key, (value.size() == 1) ? value.get(0) : value.toArray(new String[0])));
return parameters;
}
public static boolean matchesAuthorizationCodeGrantRequest(HttpServletRequest request) {
return AuthorizationGrantType.AUTHORIZATION_CODE.getValue()
.equals(request.getParameter(OAuth2ParameterNames.GRANT_TYPE))
&& request.getParameter(OAuth2ParameterNames.CODE) != null;
}
public static boolean matchesPkceTokenRequest(HttpServletRequest request) {
return matchesAuthorizationCodeGrantRequest(request)
&& request.getParameter(PkceParameterNames.CODE_VERIFIER) != null;
}
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);
}
public static String normalizeUserCode(String userCode) {
Assert.hasText(userCode, "userCode cannot be empty");
StringBuilder sb = new StringBuilder(userCode.toUpperCase().replaceAll("[^A-Z\\d]+", ""));
Assert.isTrue(sb.length() == 8, "userCode must be exactly 8 alpha/numeric characters");
sb.insert(4, '-');
return sb.toString();
}
public static boolean validateUserCode(String userCode) {
return (userCode != null && userCode.toUpperCase().replaceAll("[^A-Z\\d]+", "").length() == 8);
}
}
这样就是添加完了。
2. 认证服务器配置
认证服务器1是需要验证账号密码,2是需要生成Token。验证账号密码还是Springsecurity的逻辑,生成token用的是jwt。所以需要配置2方面。
首先我们要把数据库导进去,在maven导入的包里面,自己可以找到:
2.1 security配置
和以前说的 spring security配置差不多,要userdetail、WebSecutiryConfig、错误返回数据类等。
2.1.1 userdetail
@Slf4j
public class UserDetailService implements UserDetailsService {
@Resource
private ResourceService resourceService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Result<AuthUser> result = resourceService.loadUserByName(username);
if(!CodeMsg.SUCCESS.getCode().equals(result.getCode())) {
throw new UsernameNotFoundException("账号或密码错误!");
}
AuthUser user = BeanUtil.toBean(result.getData(), AuthUser.class) ;
MyUserDetail userDetail = new MyUserDetail();
BeanUtil.copyProperties(user, userDetail, false);
if (!userDetail.isEnabled()) {
throw new DisabledException("账号状态异常!");
}
return userDetail;
}
}
@Data
@EqualsAndHashCode(callSuper = false)
public class MyUserDetail extends AuthUser implements UserDetails {
private static final long serialVersionUID = 786868339462173799L;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getUsername() {
return this.getUname();
}
@Override
public boolean isAccountNonExpired() {
return UserDetails.super.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return UserDetails.super.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return UserDetails.super.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return this.getStatus() == 1;
}
}
2.1.2 WebSecutiryConfig
@Configuration
public class WebSecutiryConfig {
@Bean
UserDetailsService userDetailsService() {
UserDetailService userDetail = new UserDetailService();
return userDetail;
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Spring Security 安全过滤器链配置
*/
@Bean
@Order(1)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// 1. 开启认证,提前排除 不需要认证的
http.authorizeHttpRequests(authorize -> {
authorize
// 路径不用加context-path
.requestMatchers("/oauth2/**").permitAll()
// 登陆图形验证码
.requestMatchers("/captcha/**").permitAll()
// 登陆
.requestMatchers("/login/oauthlogin").permitAll()
// admin
.requestMatchers("/actuator/**").permitAll()
.requestMatchers("/instances/**").permitAll()
.anyRequest().authenticated();
})
;
// 2. 登陆方式:默认是使用security提供表单的登陆页面和方式,我们这里关闭
http.formLogin(Customizer.withDefaults());
// http.formLogin(AbstractHttpConfigurer::disable);
// 3. 登出配置
// http.logout(logout -> logout.logoutSuccessHandler(new MyLogoutSuccessHandler()));
// 4. security异常错误配置
http.exceptionHandling(exception -> {
// 未认证时 访问接口,返回错误
exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());
// 未授权时,返回错误,一般资源服务器 才会用到这个
exception.accessDeniedHandler(new MyAccessDeniedHandler());
});
// 5. csrf是默认开启的,此时对于post请求,会需要一个"_csrf"的隐藏字段传递,为了前端方便,这个关了
http.csrf(csrf -> csrf.disable());
// 6. 跨域处理
http.cors(Customizer.withDefaults());
// 7. session管理,设置同一个账号只能登陆一次.单体服务这个管用,微服务 不能用
// http.sessionManagement(session -> session.maximumSessions(1).expiredSessionStrategy(new MySessionInformationExpiredStrategy()));
return http.build();
}
/**
* Spring Security 排除路径
*/
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return (web) ->
// 不走过滤器链(swagger和静态资源js、css、html)
web.ignoring().requestMatchers(
"/webjars/**",
"/doc.html",
"/swagger-resources/**",
"/v3/api-docs/**",
"/swagger-ui/**"
);
}
}
2.1.3 返回数据处理
MyAuthenticationEntryPoint
/**
* 未认证(没有登录)时,返回异常 信息
*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final HttpMessageConverter<Object> httpResponseConverter = new MappingJackson2HttpMessageConverter();
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
if (authException instanceof InvalidBearerTokenException) {
System.out.println(authException.getMessage());
}
authException.printStackTrace();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
Result<String> res = new Result<String>().error(CodeMsg.AUTHENTICATION_FAILED);
httpResponseConverter.write(res, null, httpResponse);
}
}
MyAccessDeniedHandler
/**
* 登陆了,没有权限时,触发异常 返回信息
*/
public class MyAccessDeniedHandler implements AccessDeniedHandler {
private final HttpMessageConverter<Object> httpResponseConverter = new MappingJackson2HttpMessageConverter();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
System.out.println("=====无权限的异常处理");
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
Result<Integer> res = new Result<Integer>().error(accessDeniedException.getLocalizedMessage());
httpResponseConverter.write(res, null, httpResponse);
}
}
security的配置好了,目录这样的:
2.2 oauth2配置
oauth2.1 主要配置了3个方面,都在AuthorizationServerConfig中:
- 认证服务器使用自己写的密码模式
- token生成:公钥、私钥、自定义字段等
- token和数据库交互
2.2.1 AuthorizationServerConfig
@Configuration
public class AuthorizationServerConfig {
Logger log = LoggerFactory.getLogger(this.getClass());
private static final String KEY_ID = "jnGZxjHC54hP4ZnXrrEedtNweQ6aK29w";
@Resource
private MyOAuth2TokenJwtCustomizer jwtCustomizer;
@Resource
private RSAKeyPair rsaKeyPair;
/**
* 授权服务器端点配置
*/
@Bean
@Order(1)
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<?> tokenGenerator) throws Exception {
// 这是 http 的默认配置,可以点进去看一下,我们为了加入自己的密码模式,需要把
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
// RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
// http.securityMatcher(endpointsMatcher)
// .authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
// .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
// .apply(authorizationServerConfigurer);
// 添加password模式
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).tokenEndpoint(tokenEndpoint ->
tokenEndpoint
// 添加授权模式转换器(Converter)
.accessTokenRequestConverter(new OAuth2PasswordAuthenticationConverter())
// 添加 授权模式提供者(Provider)
.authenticationProvider(new OAuth2PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator))
// 成功响应
.accessTokenResponseHandler(new MyAuthenticationSuccessHandler())
// 失败响应
.errorResponseHandler(new MyAuthenticationFailureHandler()));
// 开启OpenID Connect 1.0协议相关端点
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
// 当使用授权码模式时,因授权码模式需要登陆,这个配置需要打开
// http.exceptionHandling(exception -> exception
// .defaultAuthenticationEntryPointFor(
// new LoginUrlAuthenticationEntryPoint("/login"),
// new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
// )
// );
return http.build();
}
/**
* 配置认证服务器请求地址
*/
@Bean
@Order(2)
AuthorizationServerSettings authorizationServerSettings() {
// 什么都不配置,则使用默认地址
return AuthorizationServerSettings.builder().build();
}
// 客户端信息表:oauth2_registered_client
@Bean
@Order(3)
RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
return registeredClientRepository;
}
// 和 token表交互数据用的:oauth2_authorization
@Bean
OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService service = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);
rowMapper.setLobHandler(new DefaultLobHandler());
ObjectMapper objectMapper = new ObjectMapper();
ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
List<Module> modules = SecurityJackson2Modules.getModules(classLoader);
objectMapper.registerModules(modules);
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
// 使用刷新模式,需要从 oauth2_authorization 表反序列化attributes字段得到用户信息(SysUserDetails)
// objectMapper.addMixIn(SysUserDetails.class, SysUserMixin.class);
objectMapper.addMixIn(Long.class, Object.class);
rowMapper.setObjectMapper(objectMapper);
service.setAuthorizationRowMapper(rowMapper);
return service;
}
// 和授权记录表交互数据用的:oauth2_authorization_consent
@Bean
OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* ========= Token部分 ========
*/
/**
* 配置 JWK,为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
* JWK详细见:https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
*/
@Bean
JWKSource<SecurityContext> jwkSource() {
RSAPublicKey publicKey = null;
RSAPrivateKey privateKey = null;
String publicKeyBase64 = rsaKeyPair.getPublicKeyBase64();
String privateKeyBase64 = rsaKeyPair.getPrivateKeyBase64();
if (StringUtils.hasText(publicKeyBase64) && StringUtils.hasText(privateKeyBase64)) {
publicKey = getPublicKey(publicKeyBase64);
privateKey = getPrivateKey(privateKeyBase64);
} else {
KeyPair keyPair = generateRsaKey();
publicKey = (RSAPublicKey) keyPair.getPublic();
privateKey = (RSAPrivateKey) keyPair.getPrivate();
log.warn("未设置生成token的秘钥!!!");
log.info("生成临时秘钥:");
log.info("\r\n===publicKey===" +
"\r\n" +
Base64.getEncoder().encodeToString(publicKey.getEncoded()) +
"\r\n===privateKey===" +
"\r\n" +
Base64.getEncoder().encodeToString(privateKey.getEncoded()));
}
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(KEY_ID)
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private RSAPublicKey getPublicKey(String publicKeyBase64) {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyBase64));
RSAPublicKey rsaPublicKey = null;
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
rsaPublicKey = (RSAPublicKey)keyFactory.generatePublic(keySpec);
} catch (Exception e) {
e.printStackTrace();
}
return rsaPublicKey;
}
private RSAPrivateKey getPrivateKey(String privateKeyBase64) {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyBase64));
RSAPrivateKey rsaPrivateKey = null;
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
rsaPrivateKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
e.printStackTrace();
}
return rsaPrivateKey;
}
/**
* 生成RSA密钥对,给上面jwkSource() 方法的提供密钥对
*/
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;
}
/**
* 配置jwt解析器
*/
@Bean
JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* 配置token生成器
*/
@Bean
OAuth2TokenGenerator<?> tokenGenerator(JWKSource<SecurityContext> jwkSource) {
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource));
// token 中加入自定义的字段内容
jwtGenerator.setJwtCustomizer(jwtCustomizer);
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}
}
2.2.2 自定义token用到的类
MyOAuth2TokenJwtCustomizer
/**
* 自定义Token包含的字段信息,
* 通过context拿到authentication和其他信息,然后再拿到Principal(userDetails数据)或者其他
*/
@Configuration
public class MyOAuth2TokenJwtCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {
@Override
public void customize(JwtEncodingContext context) {
JwtClaimsSet.Builder claims = context.getClaims();
System.out.println("自定义token字段");
Authentication authentication = context.getPrincipal();
MyUserDetail detail = (MyUserDetail) authentication.getPrincipal();
claims.claim("uid", detail.getUid());
claims.claim("rcodes", detail.getRcodes());
}
}
RSAKeyPair
@Component
public class RSAKeyPair {
@Value("${security.token.public_key_base64:null}")
private String publicKeyBase64;
@Value("${security.token.private_key_base64:null}")
private String privateKeyBase64;
public String getPublicKeyBase64() {
return publicKeyBase64;
}
public String getPrivateKeyBase64() {
return privateKeyBase64;
}
}
2.2.3 自定义返回数据类
MyAuthenticationFailureHandler
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final HttpMessageConverter<Object> httpResponseConverter = new MappingJackson2HttpMessageConverter();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
Result<String> res = new Result<String>();
if (exception instanceof UsernameNotFoundException) {
res.error(CodeMsg.USERNAME_OR_PASSWORD_ERROR);
} else if (exception instanceof OAuth2AuthenticationException) {
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
res.error(error.getErrorCode());
}
System.out.println("error走这个了: MyAuthenticationFailureHandler");
httpResponseConverter.write(res, null, httpResponse);
}
}
MyAuthenticationSuccessHandler
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
/**
* MappingJackson2HttpMessageConverter 是 Spring 框架提供的一个 HTTP 消息转换器,用于将 HTTP 请求和响应的 JSON 数据与 Java 对象之间进行转换
*/
private final HttpMessageConverter<Object> accessTokenHttpResponseConverter = new MappingJackson2HttpMessageConverter();
private Converter<OAuth2AccessTokenResponse, Map<String, Object>> accessTokenResponseParametersConverter = new DefaultOAuth2AccessTokenResponseMapConverter();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
OAuth2AccessTokenResponse.Builder builder =OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue()).tokenType(accessToken.getTokenType());
if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
}
if (refreshToken != null) {
builder.refreshToken(refreshToken.getTokenValue());
}
if (!CollectionUtils.isEmpty(additionalParameters)) {
builder.additionalParameters(additionalParameters);
}
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
Map<String, Object> tokenResponseParameters = accessTokenResponseParametersConverter.convert(accessTokenResponse);
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
this.accessTokenHttpResponseConverter.write(new Result<String>().success(tokenResponseParameters), null, httpResponse);
}
}
密码扩展基本完成了,整个结构是这样的: