Spring Security 0auth2 认证服务器和资源服务器实现
一,OAuth2开放授权协议/标准
OAuth(开放授权)是⼀个开放协议/标准,允许⽤户授权第三⽅应⽤访问他们存储在另外的服务提供者
上的信息,⽽不需要将⽤户名和密码提供给第三⽅应⽤或分享他们数据的所有内容。
允许⽤户授权第三⽅应⽤访问他们存储在另外的服务提供者上的信息,⽽不需要将⽤户名和密码提供给
第三⽅应⽤或分享他们数据的所有内容

client_id :客户端id(QQ最终相当于⼀个认证授权服务器,木瓜餐饮就相当于⼀个客户端了,所以会
给 ⼀个客户端id),相当于账号
secret:相当于密码
资源所有者(Resource Owner):可以理解为⽤户⾃⼰
客户端(Client):我们想登陆的⽹站或应⽤,⽐如淘宝
认证服务器(Authorization Server):可以理解为微信或者QQ
资源服务器(Resource Server):可以理解为微信或者QQ
二、什么情况下需要使用OAuth2
第三⽅授权登录的场景:⽐如,我们经常登录⼀些⽹站或者应⽤的时候,可以选择使⽤第三⽅授权登录
的⽅式,⽐如:微信授权登录、QQ授权登录、微博授权登录等,这是典型的 OAuth2 使⽤场景。
单点登录的场景:如果项⽬中有很多微服务或者公司内部有很多服务,可以专⻔做⼀个认证中⼼(充当
认证平台⻆⾊),所有的服务都要到这个认证中⼼做认证,只做⼀次登录,就可以在多个授权范围内的
服务中⾃由串⾏。
OAuth2的颁发Token授权⽅式
1)授权码(authorization-code)2)密码式(password)提供⽤户名+密码换取token令牌
3)隐藏式(implicit)
4)客户端凭证(client credentials)
授权码模式使⽤到了回调地址,是最复杂的授权⽅式,微博、微信、QQ等第三⽅登录就是这种模式。我
们说接⼝对接中常使⽤的password密码模式(提供⽤户名+密码换取token)。
三、Spring Cloud OAuth2 + JWT 实现
Spring Cloud OAuth2 是 Spring Cloud 体系对OAuth2协议的实现,可以⽤来做多个微服务的统⼀认证
(验证身份合法性)授权(验证权限)。通过向OAuth2服务(统⼀认证授权服务)发送某个类型的
grant_type进⾏集中认证和授权,从⽽获得access_token(访问令牌),⽽这个token是受其他微服务
信任的。
注意:使⽤OAuth2解决问题的本质是,引⼊了⼀个认证授权层,认证授权层连接了资源的拥有者,在
授权层⾥⾯,资源的拥有者可以给第三⽅应⽤授权去访问我们的某些受保护资源
1,授权服务器
1,pom 导入
<!--导入spring cloud oauth2依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.1.0.RELEASE</version>
<exclusions>
<exclusion>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.11.RELEASE</version>
</dependency>
<!--引入security对oauth2的支持-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
package com.mugua.oauth.config;
import com.mugua.oauth.service.CustomUserDetailsService;
import com.mugua.oauth.service.impl.CustomTokenServices;
import com.mugua.oauth.translator.CustomWebResponseExceptionTranslator;
import com.mugua.oauth.wrapper.CustomUserDetailsByNameServiceWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import javax.sql.DataSource;
import java.util.Arrays;
@Configuration
@EnableAuthorizationServer
@RefreshScope
public class OauthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@Autowired
private CustomUserDetailsService usersService;
@Autowired
private CustomWebResponseExceptionTranslator customWebResponseExceptionTranslator;
/**
* jwt签名密钥
*/
@Value("${spring.sign.key}")
private String signKey;
/**
* 设置令牌过期时间,一般大厂会设置为2个小时 单位为秒
*/
@Value("${token.time.tokenValidity}")
private String tokenValidity;
/**
* 设置刷新令牌的有效时间 3天
*/
@Value("${token.time.refreshTokenValidity}")
private String refreshTokenValidity;
/**
* 认证服务器最终是以api对外提供服务(校验合法性,并且生成令牌,校验令牌等)
* 那么,以api接口方式对外的话,就必然涉及到接口的访问权限,需要在这这里进行配置
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
//相当于打开endpoint访问的接口开关,这样的话后期就能访问该接口
security
//允许客户端表单认证
//主要是让/oauth/token支持client_id和client_secret做登陆认证如果开启了allowFormAuthenticationForClients,那么就在BasicAuthenticationFilter之前
//添加ClientCredentialsTokenEndpointFilter,使用ClientDetailsUserDetailsService来进行登陆认证
//这个如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走ClientCredentialsTokenEndpointFilter来保护
//如果没有支持allowFormAuthenticationForClients或者有支持但是url中没有client_id和client_secret的,走basic认证保护
.allowFormAuthenticationForClients()
//开启端口/oauth/token_key的访问权限(允许) 可以理解为生成令牌
.tokenKeyAccess("permitAll()")
//开启端口/oauth/check_token的访问权限(允许) 可以理解为校验令牌
.checkTokenAccess("permitAll()");
}
/**
* 客户端详情,比如client_id,secret
* 比如这个服务就如果QQ平台,腾讯邮箱作为客户端,需要QQ平台进行认证授权登录认证等,
* 提前需要到QQ平台注册,QQ平台会给QQ邮箱
* 颁发client_id等必要参数,表明客户端是谁
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
// 从内存中加载客户端详情改为从数据库中加载客户端详情
clients.withClientDetails(createJdbcClientDetailsService());
}
@Bean
public JdbcClientDetailsService createJdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
/**
* 认证服务器是玩转token,那么这里配置token令牌管理相关(token此时就是一个字符串当下的token需要在服务器端存储)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
endpoints
//指定token存储方法
.tokenStore(tokenStore())
//token服务的一个描述,可以认为是token生成的细节的描述,比如有效时间等
.tokenServices(customTokenServices())
.reuseRefreshTokens(true)
//指定认证管理器,随后注入一个到当前类
.authenticationManager(authenticationManager)
.userDetailsService(usersService)
//异常翻译处理
.exceptionTranslator(customWebResponseExceptionTranslator)
//请求类型
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
/**
* 该方法用于创建tokenStore对象(令牌存储对象)token以什么形式存储
*/
public TokenStore tokenStore() {
//使用jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 返回jwt令牌转换器(帮助我们⽣成jwt令牌的)在这⾥,我们可以把签名密钥传递进去给转换器对象
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
//签名密钥
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(signKey);
//验证时使⽤的密钥,和签名密钥保持⼀致
jwtAccessTokenConverter.setVerifier(new MacSigner(signKey));
return jwtAccessTokenConverter;
}
public CustomTokenServices customTokenServices() {
CustomTokenServices tokenServices = new CustomTokenServices();
//令牌存在哪里
tokenServices.setTokenStore(tokenStore());
//开启令牌刷新
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(true);
tokenServices.setClientDetailsService(createJdbcClientDetailsService());
//针对jwt令牌的添加
tokenServices.setTokenEnhancer(jwtAccessTokenConverter());
// 设置自定义的CustomUserDetailsByNameServiceWrapper
if (usersService != null) {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new CustomUserDetailsByNameServiceWrapper(usersService));
tokenServices.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));
}
//设置令牌过期时间,一般大厂会设置为2个小时 单位为秒
tokenServices.setAccessTokenValiditySeconds(Integer.parseInt(tokenValidity));
//设置刷新令牌的有效时间 //3天
tokenServices.setRefreshTokenValiditySeconds(Integer.parseInt(refreshTokenValidity));
return tokenServices;
}
}
package com.mugua.oauth.service;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
* 继承原来的UserDetailsService新增自定义方法
*
* @author liwenchao
*/
public interface CustomUserDetailsService extends UserDetailsService {
UserDetails loadUserByUsername(String username, String userType) throws UsernameNotFoundException;
@Override
default UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
package com.mugua.oauth.translator;
import com.mugua.oauth.entity.DmpResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.common.exceptions.*;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.stereotype.Component;
/**
* 异常翻译
* 就返回了500 没有返回具体的参数code码
*
* @author lwc
*/
@Slf4j
@Component
@SuppressWarnings("all")
public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator {
@Override
public ResponseEntity<?> translate(Exception e) {
ResponseEntity.BodyBuilder status = ResponseEntity.status(HttpStatus.BAD_REQUEST);
String code = null;
String message = "认证失败";
log.error(message, e);
if (e instanceof UnsupportedGrantTypeException) {
code = String.valueOf(((UnsupportedGrantTypeException) e).getHttpErrorCode());
message = "不支持该认证类型";
return status.body(DmpResult.data(code, message));
}
if (e instanceof InvalidTokenException
&& StringUtils.containsIgnoreCase(e.getMessage(), "Invalid refresh token (expired)")) {
code = String.valueOf(((InvalidTokenException) e).getHttpErrorCode());
message = "刷新令牌已过期,请重新登录";
status = ResponseEntity.status(HttpStatus.UNAUTHORIZED);
return status.body(DmpResult.data(code, message));
}
if (e instanceof InvalidScopeException) {
code = String.valueOf(((InvalidScopeException) e).getHttpErrorCode());
message = "不是有效的scope值";
return status.body(DmpResult.data(code, message));
}
if (e instanceof RedirectMismatchException) {
code = String.valueOf(((RedirectMismatchException) e).getHttpErrorCode());
message = "redirect_uri值不正确";
return status.body(DmpResult.data(code, message));
}
if (e instanceof BadClientCredentialsException) {
code = String.valueOf(((BadClientCredentialsException) e).getHttpErrorCode());
message = "client值不合法";
status = ResponseEntity.status(HttpStatus.UNAUTHORIZED);
return status.body(DmpResult.data(code, message));
}
if (e instanceof UnsupportedResponseTypeException) {
code = String.valueOf(((UnsupportedResponseTypeException) e).getHttpErrorCode());
String codeMessage = StringUtils.substringBetween(e.getMessage(), "[", "]");
message = codeMessage + "不是合法的response_type值";
return status.body(DmpResult.data(code, message));
}
if (e instanceof InvalidGrantException) {
code = String.valueOf(((InvalidGrantException) e).getHttpErrorCode());
if (StringUtils.containsIgnoreCase(e.getMessage(), "Invalid refresh token")) {
message = "refresh token无效";
return status.body(DmpResult.data(code, message));
}
if (StringUtils.containsIgnoreCase(e.getMessage(), "Invalid authorization code")) {
code = String.valueOf(((InvalidGrantException) e).getHttpErrorCode());
String codeMessage = StringUtils.substringAfterLast(e.getMessage(), ": ");
message = "授权码" + codeMessage + "不合法";
return status.body(DmpResult.data(code, message));
}
if (StringUtils.containsIgnoreCase(e.getMessage(), "locked")) {
message = "用户已被锁定,请联系管理员";
return status.body(DmpResult.data(code, message));
}
message = "用户名或密码错误";
return status.body(DmpResult.data(code, message));
}
if (e instanceof UsernameNotFoundException) {
message = e.getMessage();
code = String.valueOf(HttpStatus.UNAUTHORIZED.value());
return status.body(DmpResult.data(code, message));
}
//没翻译到的用默认
if (e instanceof Exception) {
code = String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value());
return status.body(DmpResult.data(code, message));
}
code = String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value());
return status.body(DmpResult.data(code, message));
}
}
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mugua.oauth.wrapper;
import com.mugua.oauth.service.CustomUserDetailsService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.Assert;
import java.util.Map;
/**
* This implementation for AuthenticationUserDetailsService wraps a regular Spring
* Security UserDetailsService implementation, to retrieve a UserDetails object based on
* the user name contained in an <tt>Authentication</tt> object.
*
* @author Ruud Senden
* @author Scott Battaglia
* @since 2.0
*/
public class CustomUserDetailsByNameServiceWrapper<T extends Authentication> implements AuthenticationUserDetailsService<T>, InitializingBean {
private CustomUserDetailsService userDetailsService = null;
/**
* Constructs an empty wrapper for compatibility with Spring Security 2.0.x's method
* of using a setter.
*/
public CustomUserDetailsByNameServiceWrapper() {
// constructor for backwards compatibility with 2.0
}
/**
* Constructs a new wrapper using the supplied
* {@link org.springframework.security.core.userdetails.UserDetailsService} as the
* service to delegate to.
*
* @param userDetailsService the UserDetailsService to delegate to.
*/
public CustomUserDetailsByNameServiceWrapper(final CustomUserDetailsService userDetailsService) {
Assert.notNull(userDetailsService, "userDetailsService cannot be null.");
this.userDetailsService = userDetailsService;
}
/**
* Check whether all required properties have been set.
*
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
Assert.notNull(this.userDetailsService, "UserDetailsService must be set");
}
/**
* Get the UserDetails object from the wrapped UserDetailsService implementation
*/
@Override
public UserDetails loadUserDetails(T authentication) throws UsernameNotFoundException {
// ----------添加自定义的内容----------
AbstractAuthenticationToken principal = (AbstractAuthenticationToken) authentication.getPrincipal();
Map<String, String> map = (Map<String, String>) principal.getDetails();
String userType = map.get("userType");
// ----------添加自定义的内容----------
return this.userDetailsService.loadUserByUsername(authentication.getName(), userType); // 使用自定义的userDetailsService
}
/**
* Set the wrapped UserDetailsService implementation
*
* @param aUserDetailsService The wrapped UserDetailsService to set
*/
public void setUserDetailsService(CustomUserDetailsService aUserDetailsService) {
this.userDetailsService = aUserDetailsService;
}
}
package com.mugua.oauth.encoder;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.mugua.oauth.utils.DecodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* @author liwenchao
*/
@Slf4j
public class CustomPasswordEncoder implements PasswordEncoder {
/**
* 加密(外面调用一般在注册的时候加密前端传过来的密码保存进数据库)
* 注册不在这里进行所以这里暂时不进行操作保持明文
* rawPassword == userNotFoundPassword 防止计时攻击
*
* @param rawPassword 前端传过来的密码
*/
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
/**
* 加密前后对比(一般用来比对前端提交过来的密码和数据库存储密码, 也就是明文和密文的对比)
*
* @param rawPassword 前端传过来的密码
* @param encodedPassword 数据库取出的密码
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
if (encodedPassword == null || encodedPassword.length() == 0) {
log.info("Empty encoded password");
return false;
}
//判断是否base64
boolean b = DecodeUtils.checkBase64(rawPassword.toString());
if (!b) {
return rawPassword.toString().equalsIgnoreCase(encodedPassword);
}
String passwordJson = null;
try {
passwordJson = new String(Base64.getDecoder().decode(rawPassword.toString()), StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("rawPassword {} decoder fail", passwordJson);
}
JSONObject jsonObject = JSON.parseObject(passwordJson);
String type = jsonObject.getString("p1");
String password = jsonObject.getString("p2");
if("webDefault".equalsIgnoreCase(type)){
return true;
}
JSONObject json = JSON.parseObject(encodedPassword);
String returnPwd = json.getString("p2");
String salt = json.getString("p3");
return !"default".equalsIgnoreCase(type) || DecodeUtils.matchesForUser(password, returnPwd, salt);
}
}
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mugua.oauth.provider;
import com.mugua.oauth.service.CustomUserDetailsService;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;
import java.util.Map;
/**
* An {@link AuthenticationProvider} implementation that retrieves user details from a
* {@link UserDetailsService}.
*
* @author Ben Alex
* @author Rob Winch
*/
public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
// ~ Static fields/initializers
// =====================================================================================
/**
* The plaintext password used to perform
* PasswordEncoder#matches(CharSequence, String)} on when the user is
* not found to avoid SEC-2056.
*/
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
// ~ Instance fields
// ================================================================================================
private PasswordEncoder passwordEncoder;
/**
* The password used to perform
* {@link PasswordEncoder#matches(CharSequence, String)} on when the user is
* not found to avoid SEC-2056. This is necessary, because some
* {@link PasswordEncoder} implementations will short circuit if the password is not
* in a valid format.
*/
private volatile String userNotFoundEncodedPassword;
private CustomUserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public CustomAuthenticationProvider() {
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
// ~ Methods
// ========================================================================================================
@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
@Override
protected void doAfterPropertiesSet() {
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
}
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
// 自定义添加
Map<String, String> map = (Map<String, String>) authentication.getDetails();
try {
// 自定义添加
String userType = map.get("userType");
// 自定义添加userType参数
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username, userType);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
} catch (InternalAuthenticationServiceException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
@Override
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
}
}
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
}
}
/**
* Sets the PasswordEncoder instance to be used to encode and validate passwords. If
* not set, the password will be compared using {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
*
* @param passwordEncoder must be an instance of one of the {@code PasswordEncoder}
* types.
*/
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = null;
}
protected PasswordEncoder getPasswordEncoder() {
return passwordEncoder;
}
public void setUserDetailsService(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected CustomUserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsPasswordService(
UserDetailsPasswordService userDetailsPasswordService) {
this.userDetailsPasswordService = userDetailsPasswordService;
}
}
package com.mugua.oauth.utils;
import com.alibaba.nacos.common.utils.Md5Utils;
import org.apache.shiro.crypto.hash.SimpleHash;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* @author liwenchao
*/
public class DecodeUtils {
/**
* 解密原业务线账户密码是否正确的方法
*/
public static boolean matchesForUser(String paramPwd, String userPwd, String salt) {
//拿到用户密码(明文)进行三次md5
for (int i = 0; i < 3; i++) {
paramPwd = Md5Utils.getMD5(paramPwd.getBytes(StandardCharsets.UTF_8));
}
SimpleHash md5 = new SimpleHash("MD5", paramPwd, salt, 5);
return md5.toString().equalsIgnoreCase(userPwd);
}
/**
* 解密openid方式密码
* @param paramPwd
* @param userPwd
* @return
*/
public static boolean matchesForEquals(String paramPwd,String userPwd){
return paramPwd.trim().equalsIgnoreCase(userPwd);
}
/**
* 判断字符串是否为base64编码
* Ascii码说明:共95个可读字符
* 0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符)
* 32~126(共95个)是字符(32是空格),其中48~57为0到9十个阿拉伯数字。
* 65~90为26个大写英文字母,97~122号为26个小写英文字母,其余为一些标点符号、运算符号等。
*/
public static boolean checkBase64(String str) {
//使用正则来判断是否符合base64编码的特征(但是无法排除类似于root这种特殊情况)
String base64Pattern = "^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$";
boolean isLegal = str.matches(base64Pattern);
if (isLegal) {
//对于某些字符可能符合base64编码特征,但是却不是base64编码格式,进行进一步判断,如果解码后含有乱码(即Ascii码不在32~126),
//说明虽然符合base64编码特征,但是不是base64编码,如:root
try {
String decStr = new String(Base64.getDecoder().decode(str.getBytes()), StandardCharsets.UTF_8);
char[] passArr = decStr.toCharArray();
for (char c : passArr) {
if (charToByteAscii2(c) < 32 || charToByteAscii2(c) > 126) {
return false;
}
}
} catch (Exception e) {
return false;
}
} else {
return false;
}
return true;
}
private static byte charToByteAscii2(char ch) {
return (byte) ch;
}
}
package com.mugua.oauth.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.io.Serializable;
/**
* 普通返回
*
* @author lwc
*/
public class DmpResult<T> implements Serializable {
private static final long serialVersionUID = 771836922595187944L;
//返回数据
protected T data;
//返回错误码
protected String code;
//返回消息
protected String msg;
public DmpResult() {
}
public DmpResult(T t, String code, String msg) {
this.data = t;
this.code = code;
this.msg = msg;
}
/**
* 成功返回的默认方法
*/
public static DmpResult success() {
return new DmpResult(null, StatusCodeEnum.SUC.getCode(), "result_success");
}
public static DmpResult success(String msg) {
return new DmpResult(null, StatusCodeEnum.SUC.getCode(), msg);
}
/**
* 数据返回的默认方法
*/
public static <T> DmpResult data(T t) {
return new DmpResult(t, StatusCodeEnum.SUC.getCode(), "result_success");
}
public static <T> DmpResult data(T t, String msg) {
return new DmpResult(t, StatusCodeEnum.SUC.getCode(), msg);
}
public static DmpResult data(String retCode, String msg) {
return new DmpResult(null, retCode, msg);
}
/**
* 错误的默认方法
*/
public static DmpResult failed() {
return new DmpResult(null, StatusCodeEnum.ERROR.getCode(), "result_fail");
}
public static DmpResult failed(String msg) {
return new DmpResult(null, StatusCodeEnum.ERROR.getCode(), msg);
}
public static DmpResult failed(StatusCodeEnum statusCodeEnum) {
return new DmpResult(null, statusCodeEnum.getCode(), "result_fail");
}
public static DmpResult failed(StatusCodeEnum statusCodeEnum, String msg) {
return new DmpResult(null, statusCodeEnum.getCode(), msg);
}
public static DmpResult status(boolean status) {
return status ? success() : failed();
}
@JsonIgnore
public Boolean isSuccess() {
return StatusCodeEnum.SUC.getCode().equals(this.code);
}
@JsonIgnore
public Boolean isRetData() {
return isSuccess() && this.getData() != null;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
package com.mugua.oauth.entity;
/**
* 新增枚举字段需要按照范围来申请
* <p>
*
* @author liwenchao
*/
public enum StatusCodeEnum {
// 成功
SUC("200", "result_success"),
// 失败
ERROR("500", "result_fail"),
;
//值
private String code;
//描述
private String desc;
StatusCodeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
public String getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
package com.mugua.oauth.config;
import com.mugua.oauth.encoder.CustomPasswordEncoder;
import com.mugua.oauth.provider.CustomAuthenticationProvider;
import com.mugua.oauth.service.impl.UsersServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UsersServiceImpl usersService;
/**
* 注册一个认证管理器对象给容器
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密码编码对象(暂不对密码进行加密处理),如果有加密就直接往容器里面扔,就不用这个了
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new CustomPasswordEncoder();
}
/**
* 处理用户名和密码的验证事宜
* 1.客户端传递username和password 参数到认证服务器
* 2.username 和password 会存在数据库中
* 3.根据用户表中的数据,验证当前传递过来的用户信息的合法性
*/
@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
//实例化一个用户对象(相当于数据表中的一条用户记录)
authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider());
}
/**
* 用户信息提供者
*/
@Bean(name = "customAuthenticationProvider")
public AuthenticationProvider customAuthenticationProvider() {
CustomAuthenticationProvider customAuthenticationProvider = new CustomAuthenticationProvider();
//实例化一个用户对象(相当于数据表中的一条用户记录)
customAuthenticationProvider.setUserDetailsService(usersService);
customAuthenticationProvider.setHideUserNotFoundExceptions(false);
customAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return customAuthenticationProvider;
}
}
package com.mugua.oauth.config;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import java.util.stream.Collectors;
/**
* @author liwenchao
*/
@Configuration
public class MessageConverterConfig {
@Bean
@ConditionalOnMissingBean
public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
}
}
/*
Navicat Premium Data Transfer
Source Server : nj-cdb-aaxjptkx.sql.tencentcdb.com
Source Server Type : MySQL
Source Server Version : 50736
Source Host : nj-cdb-aaxjptkx.sql.tencentcdb.com:63901
Source Schema : oauth
Target Server Type : MySQL
Target Server Version : 50736
File Encoding : 65001
Date: 07/02/2023 22:26:59
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(48) NOT NULL COMMENT '主键,必须唯一,不能为空.\n用于唯一标识每一个客户端(client); 在注册时必须填写(也可由服务端自动生成).\n对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appKey,与client_id是同一个概念.',
`resource_ids` varchar(256) DEFAULT NULL COMMENT '客户端所能访问的资源id集合,多个资源时用逗号(,)分隔,如: "unity-resource,mobile-resource".\n该字段的值必须来源于与security.xml中标签‹oauth2:resource-server的属性resource-id值一致. 在security.xml配置有几个‹oauth2:resource-server标签, 则该字段可以使用几个该值.\n在实际应用中, 我们一般将资源进行分类,并分别配置对应的‹oauth2:resource-server,如订单资源配置一个‹oauth2:resource-server, 用户资源又配置一个‹oauth2:resource-server. 当注册客户端时,根据实际需要可选择资源id,也可根据不同的注册流程,赋予对应的资源id.',
`client_secret` varchar(256) DEFAULT NULL COMMENT '用于指定客户端(client)的访问密匙; 在注册时必须填写(也可由服务端自动生成).\n对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appSecret,与client_secret是同一个概念.',
`scope` varchar(256) DEFAULT NULL COMMENT '指定客户端申请的权限范围,可选值包括read,write,trust;若有多个权限范围用逗号(,)分隔,如: "read,write".\nscope的值与security.xml中配置的‹intercept-url的access属性有关系. 如‹intercept-url的配置为\n‹intercept-url pattern="/m/**" access="ROLE_MOBILE,SCOPE_READ"/>\n则说明访问该URL时的客户端必须有read权限范围. write的配置值为SCOPE_WRITE, trust的配置值为SCOPE_TRUST.\n在实际应该中, 该值一般由服务端指定, 常用的值为read,write.',
`authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '指定客户端支持的grant_type,可选值包括authorization_code,password,refresh_token,implicit,client_credentials, 若支持多个grant_type用逗号(,)分隔,如: "authorization_code,password".\n在实际应用中,当注册时,该字段是一般由服务器端指定的,而不是由申请者去选择的,最常用的grant_type组合有: "authorization_code,refresh_token"(针对通过浏览器访问的客户端); "password,refresh_token"(针对移动设备的客户端).\nimplicit与client_credentials在实际中很少使用.',
`web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '客户端的重定向URI,可为空, 当grant_type为authorization_code或implicit时, 在Oauth的流程中会使用并检查与注册时填写的redirect_uri是否一致. 下面分别说明:\n当grant_type=authorization_code时, 第一步 从 spring-oauth-server获取 ''code''时客户端发起请求时必须有redirect_uri参数, 该参数的值必须与 web_server_redirect_uri的值一致. 第二步 用 ''code'' 换取 ''access_token'' 时客户也必须传递相同的redirect_uri.\n在实际应用中, web_server_redirect_uri在注册时是必须填写的, 一般用来处理服务器返回的code, 验证state是否合法与通过code去换取access_token值.\n在spring-oauth-client项目中, 可具体参考AuthorizationCodeController.java中的authorizationCodeCallback方法.\n当grant_type=implicit时通过redirect_uri的hash值来传递access_token值.如:\nhttp://localhost:7777/spring-oauth-client/implicit#access_token=dc891f4a-ac88-4ba6-8224-a2497e013865&token_type=bearer&expires_in=43199\n然后客户端通过JS等从hash值中取到access_token值.',
`authorities` varchar(256) DEFAULT NULL COMMENT '指定客户端所拥有的Spring Security的权限值,可选, 若有多个权限值,用逗号(,)分隔, 如: "ROLE_UNITY,ROLE_USER".\n对于是否要设置该字段的值,要根据不同的grant_type来判断, 若客户端在Oauth流程中需要用户的用户名(username)与密码(password)的(authorization_code,password),\n则该字段可以不需要设置值,因为服务端将根据用户在服务端所拥有的权限来判断是否有权限访问对应的API.\n但如果客户端在Oauth流程中不需要用户信息的(implicit,client_credentials),\n则该字段必须要设置对应的权限值, 因为服务端将根据该字段值的权限来判断是否有权限访问对应的API.\n(请在spring-oauth-client项目中来测试不同grant_type时authorities的变化)',
`access_token_validity` int(11) DEFAULT NULL COMMENT '设定客户端的access_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时).\n在服务端获取的access_token JSON数据中的expires_in字段的值即为当前access_token的有效时间值.\n在项目中, 可具体参考DefaultTokenServices.java中属性accessTokenValiditySeconds.\n在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义.',
`refresh_token_validity` int(11) DEFAULT NULL COMMENT '设定客户端的refresh_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天).\n若客户端的grant_type不包括refresh_token,则不用关心该字段 在项目中, 可具体参考DefaultTokenServices.java中属性refreshTokenValiditySeconds.\n\n在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义.',
`additional_information` varchar(4096) DEFAULT NULL COMMENT '这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的数据,如:\n{"country":"CN","country_code":"086"}\n按照spring-security-oauth项目中对该字段的描述\nAdditional information for this client, not need by the vanilla OAuth protocol but might be useful, for example,for storing descriptive information.\n(详见ClientDetails.java的getAdditionalInformation()方法的注释)在实际应用中, 可以用该字段来存储关于客户端的一些其他信息,如客户端的国家,地区,注册时的IP地址等等.',
`autoapprove` varchar(256) DEFAULT NULL COMMENT '设置用户是否自动Approval操作, 默认值为 ''false'', 可选值包括 ''true'',''false'', ''read'',''write''.\n该字段只适用于grant_type="authorization_code"的情况,当用户登录成功后,若该值为''true''或支持的scope值,则会跳过用户Approve的页面, 直接授权.\n该字段与 trusted 有类似的功能, 是 spring-security-oauth2 的 2.0 版本后添加的新属性.',
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
BEGIN;
INSERT INTO `oauth_client_details` VALUES ('mugua123', 'release,s-supply-chain-service,server-linux-service,s-supply-chain-crm,pay-gateway-service,consumption-report-statistics,marketing-activities', 'abcxyz', 'all', 'password,refresh_token', NULL, NULL, 6048000, 6048000, NULL, NULL);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
2,资源服务器
pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.1.0.RELEASE</version>
<exclusions>
<exclusion>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.11.RELEASE</version>
</dependency>
<!--引入security对oauth2的支持-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
package com.mugua.release.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* @Description
* @Author lwc
* @Data 2022/8/29 22:26
*/
@Configuration
@EnableResourceServer //开启资源服务器功能
@EnableWebSecurity //开启web访问安全
public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
// private String sign_key = "imugua20220829"; //jwt签名密钥
private String sign_key = "ee7dcc6cad12f7d7ef9642e680fdbc4d"; //jwt签名密钥
/**
* @Description 该⽅法⽤于定义资源服务器向远程认证服务器发起请求,进⾏token校验
* 等事宜
* @Param resources
* @Return
* @Author lwc
* @Date 22:29
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
/*// 设置当前资源服务的资源id
resources.resourceId("release");
// 定义token服务对象(token校验就应该靠token服务对象)
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// 校验端点/接⼝设置
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
// 携带客户端id和客户端安全码
remoteTokenServices.setClientId("clientmugua");
remoteTokenServices.setClientSecret("zbcxyz");
resources.tokenServices(remoteTokenServices);*/
//使用jwt令牌
resources.resourceId("release").tokenStore(tokenStore()).stateless(true);//无状态设置
}
/**
* @Description 场景:⼀个服务中可能有很多资源(API接⼝)
* * 某⼀些API接⼝,需要先认证,才能访问
* * 某⼀些API接⼝,压根就不需要认证,本来就是对外开放的接⼝
* * 我们就需要对不同特点的接⼝区分对待(在当前configure⽅法中
* 完成),设置是否需要经过认证
* @Param http
* @Return
* @Author lwc
* @Date 22:59
*/
@Override
public void configure(HttpSecurity http) throws Exception{
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).and().authorizeRequests()
.antMatchers("/release/**").authenticated() //需要认证
.antMatchers("/demo/**").authenticated() //需要认证
.anyRequest().permitAll(); //其余不需要认证
}
/**
* @Description 该⽅法⽤于创建tokenStore对象(令牌存储对象)
* token以什么形式存储
* @Param
* @Return {@link TokenStore}
* @Author lwc
* @Date 23:14
*/
public TokenStore tokenStore(){
//return new InMemoryTokenStore();
// 使⽤jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* @Description * 返回jwt令牌转换器(帮助我们⽣成jwt令牌的)
* * 在这⾥,我们可以把签名密钥传递进去给转换器对象
* @Param
* @Return {@link JwtAccessTokenConverter}
* @Author lwc
* @Date 23:14
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使⽤的密钥,
// 和签名密钥保持⼀致3.3.5 从数据库加载Oauth2客户端信息
// 创建数据表并初始化数据(表名及字段保持固定)
return jwtAccessTokenConverter;
}
}
获取token AND 刷新token
http://地址:端口/oauth/token?
client_secret=abcxyz&grant_type=password&username=muguauser&password=iuxyzds&client_id=mugua123
获取token携带的参数
client_id:客户端id
password:密码客户单密码
grant_type:指定使⽤哪种颁发类型,password
username:⽤户名
password:密码
三、验证

刷新token

这里是没有指定用户名和密码的,并把grant_type改为了refresh_token
验证token
