新項(xiàng)目都開始用JDK17了鄙麦,Springboot也用3.x了,對(duì)應(yīng)的認(rèn)證授權(quán)服務(wù)也要升級(jí)了镊折。Spring OAuth2.0不再支持了胯府,開始使用Spring Authorization Server了。因?yàn)槿サ袅嗣艽a模式恨胚,而我們項(xiàng)目最適用的還是密碼模式盟劫,這里我們使用OAuth2.1的擴(kuò)展功能補(bǔ)上密碼模式。
參考官方文檔与纽,給出了步驟:
此次使用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. 密碼模式
按照文檔,添加密碼模式塘装,主要添加幾個(gè)文件:
- Converter類:主要是限定請(qǐng)求Token的參數(shù)的急迂,并未驗(yàn)證參數(shù)的值,驗(yàn)證好參數(shù)后蹦肴,會(huì)將參數(shù)封裝成passwordToken實(shí)體僚碎,往下傳
- Provider類,主要作用2個(gè)阴幌,1是驗(yàn)證參數(shù)值勺阐,2是用這些數(shù)據(jù)生成Token。數(shù)據(jù)來源就是Converter類傳過來的passwordToken實(shí)體
- passwordToken實(shí)體:實(shí)體類功能矛双,有一些必須得屬性
- 密碼模式用到的工具類util
以上內(nèi)容都是參考授權(quán)碼模式或是客戶端模式來寫的渊抽。
1.1 Converter類 - OAuth2PasswordAuthenticationConverter
/**
* 限定必須要傳的參數(shù)
* 只是要這些參數(shù),并沒有對(duì)參數(shù)值校驗(yàn)
*/
public class OAuth2PasswordAuthenticationConverter implements AuthenticationConverter {
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
// 1. 提取表單參數(shù)议忽,準(zhǔn)備校驗(yàn)用懒闷,用的這個(gè)方法就是復(fù)制的sas自帶的
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getFormParameters(request);
// 2. 授權(quán)類型參數(shù) (必須)
// String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
String grantType = parameters.getFirst(OAuth2ParameterNames.GRANT_TYPE);
if (!OAuth2PasswordAuthenticationToken.PASSWORD.getValue().equals(grantType)) {
return null;
}
// 3. 令牌申請(qǐng)?jiān)L問范圍參數(shù) (可選)
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. 賬號(hào)密碼參數(shù)校驗(yàn)
// 4.1 用戶名參數(shù) (必須)
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 密碼參數(shù) (必須)
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. 客戶端憑據(jù)信息,在header 中填寫的那個(gè)
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
// 6. 參數(shù)封裝成additionalParameters放到OAuth2PasswordAuthenticationToken中傳給 PasswordAuthenticationProvider 用于驗(yàn)證值
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
/**
* 參考授權(quán)碼(主要)"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部分內(nèi)容帮辟,在這里拿出來,下面用
Map<String, Object> additionalParameters = passwordAuthenticationToken.getAdditionalParameters();
OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient(passwordAuthenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// 1. 驗(yàn)證客戶端是否支持密碼模式類型(grant_type=password)
if (!registeredClient.getAuthorizationGrantTypes().contains(passwordAuthenticationToken.getGrantType())) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE);
}
// 2. 校驗(yàn)范圍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 用戶名密碼校驗(yàn)
String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);
String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);
// 我們不自己校驗(yàn)了玩焰,用oauth2的方法校驗(yàn)
// 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("賬號(hào)或密碼錯(cuò)誤");
}
// 處理userDetails的時(shí)候昔园,我們沒有添加權(quán)限信息蔓榄,這拿不到數(shù)據(jù)
// Collection<? extends GrantedAuthority> authorities = usernamePasswordAuthentication.getAuthorities();
// for (GrantedAuthority authoriti : authorities) {
// }
// 4. 生成token
// 4.1 填充token需要的上下文數(shù)據(jù),按照授權(quán)碼模式來的
Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
// 身份驗(yàn)證成功的認(rèn)證信息(用戶名蒿赢、權(quán)限等信息)
.principal(usernamePasswordAuthentication)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(authorizedScopes)
// 授權(quán)類型
.authorizationGrantType(passwordAuthenticationToken.getGrantType())
// 授權(quán)具體對(duì)象
.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. 組裝數(shù)據(jù)入庫
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實(shí)體 - OAuth2PasswordAuthenticationToken
/**
* 這個(gè)token就是傳遞數(shù)據(jù)用的一個(gè)實(shí)體润樱,想要什么數(shù)據(jù)可以寫成變量
* 在converter生成時(shí)賦值,在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;
/**
* 密碼模式身份驗(yàn)證令牌
*
* @param clientPrincipal 客戶端信息
* @param scopes 令牌申請(qǐng)?jiān)L問范圍
* @param additionalParameters 自定義額外參數(shù)(用戶名和密碼等)
*/
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());
}
/**
* 這個(gè)方法 父類中直接返回了空字符串羡棵,
* 我們可以根據(jù)自己的需要重寫壹若,可以返回密碼
*/
@Override
public Object getCredentials() {
return this.getAdditionalParameters().get(OAuth2ParameterNames.PASSWORD);
}
public Set<String> getScopes() {
return this.scopes;
}
}
1.4 util類
有2個(gè),
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. 認(rèn)證服務(wù)器配置
認(rèn)證服務(wù)器1是需要驗(yàn)證賬號(hào)密碼店展,2是需要生成Token。驗(yàn)證賬號(hào)密碼還是Springsecurity的邏輯秃流,生成token用的是jwt赂蕴。所以需要配置2方面。
首先我們要把數(shù)據(jù)庫導(dǎo)進(jìn)去舶胀,在maven導(dǎo)入的包里面概说,自己可以找到:
2.1 security配置
和以前說的 spring security配置差不多,要userdetail嚣伐、WebSecutiryConfig糖赔、錯(cuò)誤返回?cái)?shù)據(jù)類等。
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("賬號(hào)或密碼錯(cuò)誤轩端!");
}
AuthUser user = BeanUtil.toBean(result.getData(), AuthUser.class) ;
MyUserDetail userDetail = new MyUserDetail();
BeanUtil.copyProperties(user, userDetail, false);
if (!userDetail.isEnabled()) {
throw new DisabledException("賬號(hào)狀態(tài)異常放典!");
}
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. 開啟認(rèn)證,提前排除 不需要認(rèn)證的
http.authorizeHttpRequests(authorize -> {
authorize
// 路徑不用加context-path
.requestMatchers("/oauth2/**").permitAll()
// 登陸圖形驗(yàn)證碼
.requestMatchers("/captcha/**").permitAll()
// 登陸
.requestMatchers("/login/oauthlogin").permitAll()
// admin
.requestMatchers("/actuator/**").permitAll()
.requestMatchers("/instances/**").permitAll()
.anyRequest().authenticated();
})
;
// 2. 登陸方式:默認(rèn)是使用security提供表單的登陸頁面和方式基茵,我們這里關(guān)閉
http.formLogin(Customizer.withDefaults());
// http.formLogin(AbstractHttpConfigurer::disable);
// 3. 登出配置
// http.logout(logout -> logout.logoutSuccessHandler(new MyLogoutSuccessHandler()));
// 4. security異常錯(cuò)誤配置
http.exceptionHandling(exception -> {
// 未認(rèn)證時(shí) 訪問接口奋构,返回錯(cuò)誤
exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());
// 未授權(quán)時(shí),返回錯(cuò)誤拱层,一般資源服務(wù)器 才會(huì)用到這個(gè)
exception.accessDeniedHandler(new MyAccessDeniedHandler());
});
// 5. csrf是默認(rèn)開啟的弥臼,此時(shí)對(duì)于post請(qǐng)求,會(huì)需要一個(gè)"_csrf"的隱藏字段傳遞舱呻,為了前端方便醋火,這個(gè)關(guān)了
http.csrf(csrf -> csrf.disable());
// 6. 跨域處理
http.cors(Customizer.withDefaults());
// 7. session管理悠汽,設(shè)置同一個(gè)賬號(hào)只能登陸一次.單體服務(wù)這個(gè)管用,微服務(wù) 不能用
// http.sessionManagement(session -> session.maximumSessions(1).expiredSessionStrategy(new MySessionInformationExpiredStrategy()));
return http.build();
}
/**
* Spring Security 排除路徑
*/
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return (web) ->
// 不走過濾器鏈(swagger和靜態(tài)資源js芥驳、css柿冲、html)
web.ignoring().requestMatchers(
"/webjars/**",
"/doc.html",
"/swagger-resources/**",
"/v3/api-docs/**",
"/swagger-ui/**"
);
}
}
2.1.3 返回?cái)?shù)據(jù)處理
MyAuthenticationEntryPoint
/**
* 未認(rèn)證(沒有登錄)時(shí),返回異常 信息
*/
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
/**
* 登陸了兆旬,沒有權(quán)限時(shí)假抄,觸發(fā)異常 返回信息
*/
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("=====無權(quán)限的異常處理");
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個(gè)方面丽猬,都在AuthorizationServerConfig中:
- 認(rèn)證服務(wù)器使用自己寫的密碼模式
- token生成:公鑰宿饱、私鑰、自定義字段等
- token和數(shù)據(jù)庫交互
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;
/**
* 授權(quán)服務(wù)器端點(diǎn)配置
*/
@Bean
@Order(1)
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<?> tokenGenerator) throws Exception {
// 這是 http 的默認(rèn)配置脚祟,可以點(diǎn)進(jìn)去看一下谬以,我們?yōu)榱思尤胱约旱拿艽a模式,需要把
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
// 添加授權(quán)模式轉(zhuǎn)換器(Converter)
.accessTokenRequestConverter(new OAuth2PasswordAuthenticationConverter())
// 添加 授權(quán)模式提供者(Provider)
.authenticationProvider(new OAuth2PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator))
// 成功響應(yīng)
.accessTokenResponseHandler(new MyAuthenticationSuccessHandler())
// 失敗響應(yīng)
.errorResponseHandler(new MyAuthenticationFailureHandler()));
// 開啟OpenID Connect 1.0協(xié)議相關(guān)端點(diǎn)
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
// 當(dāng)使用授權(quán)碼模式時(shí)由桌,因授權(quán)碼模式需要登陸为黎,這個(gè)配置需要打開
// http.exceptionHandling(exception -> exception
// .defaultAuthenticationEntryPointFor(
// new LoginUrlAuthenticationEntryPoint("/login"),
// new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
// )
// );
return http.build();
}
/**
* 配置認(rèn)證服務(wù)器請(qǐng)求地址
*/
@Bean
@Order(2)
AuthorizationServerSettings authorizationServerSettings() {
// 什么都不配置,則使用默認(rèn)地址
return AuthorizationServerSettings.builder().build();
}
// 客戶端信息表:oauth2_registered_client
@Bean
@Order(3)
RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
return registeredClientRepository;
}
// 和 token表交互數(shù)據(jù)用的: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;
}
// 和授權(quán)記錄表交互數(shù)據(jù)用的: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)提供加密密鑰,用于加密/解密或簽名/驗(yàn)簽
* JWK詳細(xì)見: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("未設(shè)置生成token的秘鑰M扪?婚荨!");
log.info("生成臨時(shí)秘鑰:");
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密鑰對(duì)捌斧,給上面jwkSource() 方法的提供密鑰對(duì)
*/
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 中加入自定義的字段內(nèi)容
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數(shù)據(jù))或者其他
*/
@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 自定義返回?cái)?shù)據(jù)類
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走這個(gè)了: MyAuthenticationFailureHandler");
httpResponseConverter.write(res, null, httpResponse);
}
}
MyAuthenticationSuccessHandler
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
/**
* MappingJackson2HttpMessageConverter 是 Spring 框架提供的一個(gè) HTTP 消息轉(zhuǎn)換器捞蚂,用于將 HTTP 請(qǐng)求和響應(yīng)的 JSON 數(shù)據(jù)與 Java 對(duì)象之間進(jìn)行轉(zhuǎn)換
*/
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);
}
}
密碼擴(kuò)展基本完成了经瓷,整個(gè)結(jié)構(gòu)是這樣的: