本文借鉴了网上一些资料,如有侵权,请指出
1. 授权中心配置
所需依赖
<!-- spring security oauth2 开放授权 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
<!-- security-oauth2自动装配依赖 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.5.6</version>
</dependency>
<!-- security starter依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.6</version>
</dependency>
<!-- Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>
1.1 认证服务器配置
本次采用Jwt+ Redis方式生成和存储授权信息
关键注解 @EnableAuthorizationServer
/**
* @date 2020/07/21
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("oauth_client_details")
public class OauthClientDetails implements Serializable {
@TableId
private String appId;
/**
* 应用名称
*/
private String appName;
/**
* 应用key
*/
@AttributeUrlParam(name = "clientId")
private String appKey;
/**
* 应用密钥
*/
private String appSecret;
/**
* 资源id集合
*/
private String resourceIds;
/**
* oauth权限范围
*/
@AttributeUrlParam(name = "scope")
private String scope;
/**
* 支持的授权类型
*/
@AttributeUrlParam(name = "grantTypes")
private String authorizedGrantTypes;
/**
* 重定向uri
*/
@AttributeUrlParam(name = "redirectUri")
private String redirectUri;
/**
* security权限值
*/
private String authorities;
/**
* access_token有效时间
*/
private Integer accessTokenValidity;
/**
* refresh_token有效时间
*/
private Integer refreshTokenValidity;
/**
* 客户端的其他信息,必须为json格式
*/
@AttributeUrlParam(name = "additionalInformation")
private String additionalInformation;
/**
* 是否已存档
*/
private Integer archived;
/**
* 是否是信任的,0:不受信任,1:信任的
*/
private Integer trusted;
/**
* 是否自动跳过approve操作,默认为false,取值范围:true,false,read,write
*/
@AttributeUrlParam(name = "autoapprove")
private String autoapprove;
/**
* 创建人
*/
private String createBy;
/**
* 创建时间
*/
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 更新人
*/
private String updateBy;
/**
* 更新时间
*/
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
1.1.1 重写原生方法支持redis缓存 授权信息
/**
* 重写原生方法支持redis缓存 授权信息
* OauthClientDetailsMapper 就是Mapper层
*/
@Slf4j
@Service
public class RedisClientDetailsService extends ServiceImpl<OauthClientDetailsMapper, OauthClientDetails> implements ClientDetailsService {
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
// 优先从redis缓存中获取,不存在则从数据库中获取
OauthClientDetails oauthClientDetails = RedisUtils.getCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientId);
if (StringUtils.isNull(oauthClientDetails)) {
LambdaQueryWrapper<OauthClientDetails> query = new LambdaQueryWrapper<>();
query.eq(OauthClientDetails::getAppKey, clientId);
oauthClientDetails = getOne(query);
if (StringUtils.isNull(oauthClientDetails)) {
return null;
}
RedisUtils.setCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientId, oauthClientDetails, oauthClientDetails.getAccessTokenValidity(), TimeUnit.MINUTES);
}
return new ClientDetailsAdapter(oauthClientDetails);
}
}
/**
* 使用了对象适配器模式
* OauthClientDetails数据库实体对象 到 Security Oauth2提供的ClientDetails的对象适配器
*
* @date 2022/03/23
*/
public class ClientDetailsAdapter implements ClientDetails {
private final OauthClientDetails clientDetails;
public ClientDetailsAdapter(OauthClientDetails clientDetails) {
this.clientDetails = clientDetails;
}
@Override
public String getClientId() {
return clientDetails.getAppKey();
}
@Override
public Set<String> getResourceIds() {
return CollectionUtil.getSetBySplit(clientDetails.getResourceIds());
}
@Override
public boolean isSecretRequired() {
return true;
}
@Override
public String getClientSecret() {
return clientDetails.getAppSecret();
}
/**
* 客户端是否为特定范围,如果该值返回false,则忽略身份认证的请求范围(scope的值)
*
* @return
*/
@Override
public boolean isScoped() {
return true;
}
/**
* 客户端拥有的授权范围
*
* @return
*/
@Override
public Set<String> getScope() {
return CollectionUtil.getSetBySplit(clientDetails.getScope());
}
/**
* 客户端拥有的授权方式
*
* @return
*/
@Override
public Set<String> getAuthorizedGrantTypes() {
return CollectionUtil.getSetBySplit(clientDetails.getAuthorizedGrantTypes());
}
@Override
public Set<String> getRegisteredRedirectUri() {
return CollectionUtil.getSetBySplit(clientDetails.getRedirectUri());
}
@Override
public Collection<GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list = new ArrayList<>();
for (String item : CollectionUtil.getSetBySplit(clientDetails.getAuthorities())) {
GrantedAuthority authority = new SimpleGrantedAuthority(item);
list.add(authority);
}
return list;
}
@Override
public Integer getAccessTokenValiditySeconds() {
return clientDetails.getAccessTokenValidity();
}
@Override
public Integer getRefreshTokenValiditySeconds() {
return clientDetails.getRefreshTokenValidity();
}
@Override
public boolean isAutoApprove(String scope) {
return false;
}
@Override
public Map<String, Object> getAdditionalInformation() {
return null;
}
/**
* 第三应用是否可信任
*
* @return
*/
public boolean isTrusted() {
return clientDetails.getTrusted() == 1;
}
public OauthClientDetails getClientDetails() {
return clientDetails;
}
}
1.1.2 认证服务器 配置
/**
* 认证服务器
*
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private RedisClientDetailsService oauthClientDetailsService;
/**
* 定义授权和令牌端点以及令牌服务
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
// 请求方式 post
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST)
// 指定token存储位置
.tokenStore(tokenStore())
.tokenServices(tokenService())
// 用户账号密码认证
.userDetailsService(userDetailsService)
// 用户授权确认处理器
.userApprovalHandler(userApprovalHandler())
// 如果用密码模式验证,则需要配置 指定认证管理器
.authenticationManager(authenticationManager)
// 自定义异常处理
.exceptionTranslator(new CustomWebResponseExceptionTranslator());
}
/**
* 在JWT编码的令牌值和OAuth身份验证信息(双向)之间转换的助手。
* 授予令牌时,还充当TokenEnhancer
* JWT令牌转换器可以配置密钥对
* 密钥对需要密钥文件,即( .jks 或者 .keystore) 文件,
* 件里保存了密钥信息,密钥包含了私钥和公钥
*
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new MyJwt();
converter.setSigningKey(SecurityConstants.JWT_ACCESS_SIGNING_KEY);
return converter;
}
/**
* 令牌(token)管理服务
*/
@Bean
public AuthorizationServerTokenServices tokenService(){
DefaultTokenServices services = new DefaultTokenServices();
// 客户端详情服务
// 因为是向客户端颁发令牌,所以需要知道是哪一个客户端
services.setClientDetailsService(oauthClientDetailsService);
// 令牌存储策略
services.setTokenStore(tokenStore());
//令牌增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new LinkedList<>();
delegates.add(jwtAccessTokenConverter());
// delegates.add(tokenEnhancer());
tokenEnhancerChain.setTokenEnhancers(delegates);
services.setTokenEnhancer(tokenEnhancerChain);
// 支持刷新令牌
services.setSupportRefreshToken(true);
// 是否重复使用 refresh_token
services.setReuseRefreshToken(true);
return services;
}
/**
* 配置令牌端点(Token Endpoint)的安全约束
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
/**
* 默认访问安全规则是denyAll(),即默认情况下是关闭的,请求调不通
* 在资源服务器中,可能会调用oauth/token_key 来请求获取公钥,如果这里调不通,
* 资源服务就没办法获取公钥,就没办法对jwt令牌的签名算法进行解密
*/
// 允许 /oauth/token的端点表单认证
oauthServer.allowFormAuthenticationForClients()
.tokenKeyAccess("isAuthenticated()")
// 允许 /oauth/token_check端点的访
.checkTokenAccess("permitAll()")
.addTokenEndpointAuthenticationFilter(new CorsFilter(source));
}
/**
* 客户端配置
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(oauthClientDetailsService);
}
/**
* 基于 Redis 实现,令牌保存到缓存
*/
@Bean
public TokenStore tokenStore() {
RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
// 前缀
tokenStore.setPrefix(Oauth2Constants.OAUTH_ACCESS);
// 序列化策略,使用fastjson
tokenStore.setSerializationStrategy(new FastJsonRedisTokenStoreSerializationStrategy());
return tokenStore;
}
@Bean
public OAuth2RequestFactory oAuth2RequestFactory() {
return new DefaultOAuth2RequestFactory(oauthClientDetailsService);
}
@Bean
public UserApprovalHandler userApprovalHandler() {
OauthUserApprovalHandler userApprovalHandler = new OauthUserApprovalHandler();
userApprovalHandler.setTokenStore(tokenStore());
userApprovalHandler.setClientDetailsService(oauthClientDetailsService);
userApprovalHandler.setRequestFactory(oAuth2RequestFactory());
return userApprovalHandler;
}
}
1.1.3 增强令牌实现
/**
* 定制 jwt 自定义token信息
* @date 2022/4/21 0:41
*/
public class MyJwt extends JwtAccessTokenConverter {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
//设置登录人缓存信息
LoginUser loginUser = SecurityUtils.getLoginUser();
//获取客户端clientId
LinkedHashMap<String, Object> clientDetail = (LinkedHashMap<String, Object>) authentication.getUserAuthentication().getDetails();
//查询缓存中应用配置信息
OauthClientDetails oauthClientDetails = RedisUtils.getCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientDetail.get(Oauth2Constants.CLIENT_ID));
Map<String, Object> additionalInfo = new LinkedHashMap<>();
Map<String, Object> info = new LinkedHashMap<>();
info.put(SecurityConstants.DETAILS_LICENSE, SecurityConstants.LICENSE);
info.put(Oauth2Constants.TOKEN_KRY, loginUser.getTokenKey());
additionalInfo.put("info", info);
//设置token的过期时间 分钟数
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.MINUTE, oauthClientDetails.getAccessTokenValidity());
loginUser.setExpireTime(nowTime.getTime());
//缓存登录人信息
RedisUtils.setCacheObject(RedisUtils.getTokenKey(loginUser.getTokenKey()), loginUser, oauthClientDetails.getAccessTokenValidity(), TimeUnit.MINUTES);
//设置Oauth2
((DefaultOAuth2AccessToken) accessToken).setExpiration(nowTime.getTime());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return super.enhance(accessToken, authentication);
}
}
1.2 资源服务器配置
/**
* Oauth授权服务器配置,Oauth资源服务器配置
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "USER-RESOURCE";
@Autowired
private OauthTokenExtractor oauthTokenExtractor;
@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;
@Autowired
private CustomAuthExceptionEntryPoint exceptionEntryPoint;
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
public void configure(HttpSecurity http) throws Exception {
// 所有请求必须认证通过
http.authorizeRequests()
// 过滤放行
.antMatchers("/v2/api-docs").permitAll()
.antMatchers("/swagger-resources/configuration/ui").permitAll()
.antMatchers("/swagger-resources").permitAll()
.antMatchers("/swagger-resources/configuration/security").permitAll()
.antMatchers("/swagger-ui.html").permitAll()
.anyRequest().authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID).stateless(false);
// token提取器
resources.tokenExtractor(oauthTokenExtractor)
// token异常处理器
.authenticationEntryPoint(exceptionEntryPoint)
// 无权限异常处理器
.accessDeniedHandler(accessDeniedHandler);
}
}
1.2.1 oauth资源服务器的token提取器
/**
* oauth资源服务器的token提取器,用于获取请求中的access_token
* 如果没有什么特殊要求可以直接使用 {@link org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor}
*/
@Slf4j
@Component
public class OauthTokenExtractor implements TokenExtractor {
@Override
public Authentication extract(HttpServletRequest request) {
String tokenValue = this.extractToken(request);
if (tokenValue != null) {
return new PreAuthenticatedAuthenticationToken(tokenValue, "");
} else {
return null;
}
}
public String extractToken(HttpServletRequest request) {
String token = this.extractHeaderToken(request);
if (token == null) {
log.debug("Token not found in headers. Trying request parameters.");
token = request.getParameter(OAuth2AccessToken.ACCESS_TOKEN);
if (token == null) {
log.debug("Token not found in request parameters. Trying request cookies.");
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, OAuth2AccessToken.BEARER_TYPE);
token = extractCookieToken(request);
if (token == null) {
log.debug("Token not found in cookies. Not an OAuth2 request.");
} else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, "Bearer");
}
}
}
return token;
}
private String extractHeaderToken(HttpServletRequest request) {
Enumeration<String> headers = request.getHeaders(Oauth2Constants.TOKEN_HEADER);
while (headers.hasMoreElements()) {
//通常只有一个(大多数服务器强制执行)
String value = headers.nextElement();
if ((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim();
// 将此添加到此处,以获取稍后的身份验证详细信息。最好更改此方法的签名。
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, OAuth2AccessToken.BEARER_TYPE.length()).trim());
int commaIndex = authHeaderValue.indexOf(',');
if (commaIndex > 0) {
authHeaderValue = authHeaderValue.substring(0, commaIndex);
}
return authHeaderValue;
}
}
return null;
}
private String extractCookieToken(HttpServletRequest request) {
String cookieToken = null;
//根据请求数据,找到cookie数组
Cookie[] cookies = request.getCookies();
if (null != cookies && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (null != cookie.getName() && cookie.getName().trim().equalsIgnoreCase(OAuth2AccessToken.ACCESS_TOKEN)) {
cookieToken = cookie.getValue().trim();
break;
}
}
}
return cookieToken;
}
}
1.2.2 自定义访问无权限资源时的异常
/**
* 自定义访问无权限资源时的异常
*
*/
@Slf4j
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
log.info("权限不足,请联系管理员 {}", request.getRequestURI());
int code = HttpStatus.FORBIDDEN;
String msg = accessDeniedException.getMessage();
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
}
}
1.2.3 自定义未登录处理
/**
* 自定义未登录处理
* AuthenticationEntryPoint是Spring Security Web一个概念模型接口,
* 顾名思义,他所建模的概念是:“认证入口点”。
* 它在用户请求处理过程中遇到认证异常时,
* 被ExceptionTranslationFilter用于开启特定认证方案(authentication schema)的认证流程。
*/
@Slf4j
@Component
public class CustomAuthExceptionEntryPoint implements AuthenticationEntryPoint {
@Autowired
private RedisClientDetailsService clientDetailsService;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
log.info("令牌不合法,禁止访问 {}", request.getRequestURI());
//获得请求参数
String clientId = request.getParameter(Oauth2Constants.CLIENT_ID);
if(StringUtils.isBlank(clientId)){
clientId = request.getParameter(Oauth2Constants.MY_CLIENT_ID);
}
// 查询客户端id 优先从redis缓存中获取,不存在则从数据库中获取
// 优先从redis缓存中获取,不存在则从数据库中获取
OauthClientDetails oauthClientDetails = RedisUtils.getCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientId);
if (StringUtils.isNull(oauthClientDetails)) {
LambdaQueryWrapper<OauthClientDetails> lqw = Wrappers.lambdaQuery();
oauthClientDetails = clientDetailsService.getOne(lqw.eq(OauthClientDetails::getAppKey, clientId));
if (StringUtils.isNull(oauthClientDetails)) {
ServletUtils.renderString(response, JSONObject.toJSONString(AjaxResult.error(HttpStatus.UNAUTHORIZED, "未识别应用",null)));
return;
}else {
RedisUtils.setCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientId, oauthClientDetails, oauthClientDetails.getAccessTokenValidity(), TimeUnit.SECONDS);
}
}
String param = StringUtils.formatUrlParamsWithAnnocation(oauthClientDetails);
String msg = authException.getMessage();
//重定向到授权登录页
String authUrl = "http://localhost:8200/auth/login?" + param;
ServletUtils.renderString(response, JSONObject.toJSONString(AjaxResult.error(HttpStatus.UNAUTHORIZED, msg, authUrl)));
}
}
1.2.4 自定义实体类属性转换为url参数
/**
* 将实体类clazz的属性转换为url参数
* @param clazz 参数实体类
*
*/
public static String formatUrlParamsWithAnnocation(Object clazz) {
// 遍历属性类、属性值
Field[] fields = clazz.getClass().getDeclaredFields();
StringBuilder requestURL = new StringBuilder();
try {
boolean flag = true;
String property, value;
for (Field field : fields) {
//获得注解
// 容许访问私有变量
field.setAccessible(true);
boolean annotationPresent = field.isAnnotationPresent(AttributeUrlParam.class);
if(annotationPresent){
//获得注解
AttributeUrlParam urlParam = field.getAnnotation(AttributeUrlParam.class);
// 属性名
property = urlParam.name();
// 属性值
value = field.get(clazz).toString();
String params = property + "=" + value;
if (flag) {
requestURL.append(params);
flag = false;
} else {
requestURL.append("&").append(params);
}
}
}
} catch (Exception ignored) {
}
return requestURL.toString();
}
自定义注解
import java.lang.annotation.*;
/**
* 自定义注解拼接Url参数
* @author smartby
*/
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AttributeUrlParam {
/**
* Url参数名
*/
String name() default "";
}
/**
* 开启Spring Security方法级安全
*
* prePostEnabled: 确定 前置注解[@PreAuthorize,@PostAuthorize,..] 是否启用
* securedEnabled: 确定安全注解 [@Secured] 是否启用
* jsr250Enabled: 确定 JSR-250注解 [@RolesAllowed..]是否启用
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理类
*/
@Autowired
private CustomAuthExceptionEntryPoint unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 解决 无法直接注入 AuthenticationManager
*
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage").anonymous()
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
.antMatchers("/profile/**").anonymous()
.antMatchers("/auth/**").anonymous()
.antMatchers("/doc.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/webjars/**").anonymous()
.antMatchers("/*/api-docs").anonymous()
.antMatchers("/druid/**").anonymous()
// Spring Boot Actuator 的安全配置
.antMatchers("/actuator").anonymous()
.antMatchers("/actuator/**").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
/*** 设置不拦截规则 */
@Override
public void configure(WebSecurity web) {
web.ignoring()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage")
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js"
)
.antMatchers("/profile/**")
.antMatchers("/auth/**")
.antMatchers("/doc.html")
.antMatchers("/swagger-resources/**")
.antMatchers("/webjars/**")
.antMatchers("/*/api-docs")
.antMatchers("/druid/**")
// Spring Boot Actuator 的安全配置
.antMatchers("/actuator")
.antMatchers("/actuator/**")
.antMatchers( "/v2/**", "/swagger-resources/**");
}
}
1.3 其他一些工具类
1.3.1 自定义默认的刷新token序列化工具类
/**
* 自定义默认的刷新token序列化工具类
*
* @date 2022/03/01
*/
public class DefaultOauth2RefreshTokenSerializer implements ObjectDeserializer {
@Override
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
if (type == DefaultOAuth2RefreshToken.class) {
JSONObject jsonObject = parser.parseObject();
String tokenId = jsonObject.getString("value");
DefaultOAuth2RefreshToken refreshToken = new DefaultOAuth2RefreshToken(tokenId);
return (T) refreshToken;
}
return null;
}
@Override
public int getFastMatchToken() {
return 0;
}
}
1.3.2 redis存储json格式序列化反序列化工具类
/**
* fastjson redis存储json格式序列化反序列化工具类
*/
public class FastJsonRedisTokenStoreSerializationStrategy implements RedisTokenStoreSerializationStrategy {
private final static ParserConfig DEFAULT_REDIS_CONFIG = new ParserConfig();
static {
// 自定义oauth2序列化,
DEFAULT_REDIS_CONFIG.setAutoTypeSupport(true);
自定义DefaultOauth2RefreshTokenSerializer反序列化
DEFAULT_REDIS_CONFIG.putDeserializer(DefaultOAuth2RefreshToken.class, new DefaultOauth2RefreshTokenSerializer());
自定义OAuth2Authentication反序列化
DEFAULT_REDIS_CONFIG.putDeserializer(OAuth2Authentication.class, new OAuth2AuthenticationSerializer());
//添加autotype白名单
DEFAULT_REDIS_CONFIG.addAccept("org.springframework.security.oauth2.provider.");
DEFAULT_REDIS_CONFIG.addAccept("org.springframework.security.oauth2.provider.client");
TypeUtils.addMapping("org.springframework.security.oauth2.provider.OAuth2Authentication", OAuth2Authentication.class);
TypeUtils.addMapping("org.springframework.security.oauth2.provider.client.BaseClientDetails", BaseClientDetails.class);
DEFAULT_REDIS_CONFIG.addAccept("org.springframework.security.oauth2.common.");
TypeUtils.addMapping("org.springframework.security.oauth2.common.DefaultOAuth2AccessToken", DefaultOAuth2AccessToken.class);
TypeUtils.addMapping("org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken", DefaultExpiringOAuth2RefreshToken.class);
DEFAULT_REDIS_CONFIG.addAccept("com.pthin.oauth.server.adapter");
TypeUtils.addMapping("com.pthin.oauth.server.adapter.UserDetailsAdapter", LoginUser.class);
DEFAULT_REDIS_CONFIG.addAccept("org.springframework.security.web.authentication.preauth");
TypeUtils.addMapping("org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken", PreAuthenticatedAuthenticationToken.class);
}
@Override
public <T> T deserialize(byte[] bytes, Class<T> clazz) {
Assert.notNull(clazz, "clazz can't be null");
if (bytes == null || bytes.length == 0) {
return null;
}
try {
return JSON.parseObject(new String(bytes, IOUtils.UTF8), clazz, DEFAULT_REDIS_CONFIG);
} catch (Exception e) {
throw new SerializationException("Could not serialize: " + e.getMessage(), e);
}
}
@Override
public String deserializeString(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return null;
}
return new String(bytes, IOUtils.UTF8);
}
@Override
public byte[] serialize(Object object) {
if (object == null) {
return new byte[0];
}
try {
return JSON.toJSONBytes(object, SerializerFeature.WriteClassName, SerializerFeature.DisableCircularReferenceDetect);
} catch (Exception e) {
throw new SerializationException("Could not serialize: " + e.getMessage(), e);
}
}
@Override
public byte[] serialize(String data) {
if (StringUtils.isEmpty(data)) {
return new byte[0];
}
return data.getBytes(IOUtils.UTF8);
}
}
序列化
/**
* @date 2020/07/29
*/
public class OAuth2AuthenticationSerializer implements ObjectDeserializer {
@Override
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
if (type == OAuth2Authentication.class) {
try {
Object o = parse(parser);
if (o == null) {
return null;
} else if (o instanceof OAuth2Authentication) {
return (T) o;
}
JSONObject jsonObject = (JSONObject) o;
OAuth2Request request = parseOAuth2Request(jsonObject);
//判断json节点userAuthentication的类型,根据类型动态取值
//UsernamePasswordAuthenticationToken 密码模式/授权码模式下,存储类型为UsernamePasswordAuthenticationToken
//PreAuthenticatedAuthenticationToken 刷新token模式下,存储类型为PreAuthenticatedAuthenticationToken
Object autoType = jsonObject.get("userAuthentication");
return (T) new OAuth2Authentication(request, jsonObject.getObject("userAuthentication", (Type) autoType.getClass()));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
return null;
}
@Override
public int getFastMatchToken() {
return 0;
}
private OAuth2Request parseOAuth2Request(JSONObject jsonObject) {
JSONObject json = jsonObject.getObject("oAuth2Request", JSONObject.class);
Map<String, String> requestParameters = json.getObject("requestParameters", Map.class);
String clientId = json.getString("clientId");
String grantType = json.getString("grantType");
String redirectUri = json.getString("redirectUri");
Boolean approved = json.getBoolean("approved");
Set<String> responseTypes = json.getObject("responseTypes", new TypeReference<HashSet<String>>() {
});
Set<String> scope = json.getObject("scope", new TypeReference<HashSet<String>>() {
});
Set<String> authorities = json.getObject("authorities", new TypeReference<HashSet<String>>() {
});
Set<GrantedAuthority> grantedAuthorities = new HashSet<>(0);
if (authorities != null && !authorities.isEmpty()) {
authorities.forEach(s -> grantedAuthorities.add(new SimpleGrantedAuthority(s)));
}
Set<String> resourceIds = json.getObject("resourceIds", new TypeReference<HashSet<String>>() {
});
Map<String, Serializable> extensions = json.getObject("extensions", new TypeReference<HashMap<String, Serializable>>() {
});
OAuth2Request request = new OAuth2Request(requestParameters, clientId, grantedAuthorities, approved, scope, resourceIds, redirectUri, responseTypes, extensions);
TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scope, grantType);
request.refresh(tokenRequest);
return request;
}
private Object parse(DefaultJSONParser parse) {
JSONObject object = new JSONObject(parse.lexer.isEnabled(Feature.OrderedField));
Object parsedObject = parse.parseObject(object);
if (parsedObject instanceof JSONObject) {
return (JSONObject) parsedObject;
} else if (parsedObject instanceof OAuth2Authentication) {
return parsedObject;
} else {
return parsedObject == null ? null : new JSONObject((Map) parsedObject);
}
}
}
1.4 自定义处理器
1.4.1 认证成功处理
/**
* 认证成功处理
*
*/
@Slf4j
@Component
public class AuthenticationSuccessEventHandler implements ApplicationListener<AuthenticationSuccessEvent> {
@Autowired
private LogininforService asyncService;
@Override
public void onApplicationEvent(AuthenticationSuccessEvent event) {
Authentication authentication = (Authentication) event.getSource();
if (StringUtils.isNotNull(authentication.getPrincipal()) && authentication.getPrincipal() instanceof LoginUser) {
HttpServletRequest request = ServletUtils.getRequest();
String url = request.getRequestURI();
LoginUser user = (LoginUser) authentication.getPrincipal();
String username = user.getUsername();
log.info("用户:{} 授权成功,url:{}", username, url);
// 登录日志记录
if (StringUtils.containsAny(url, SecurityConstants.AUTH_TOKEN, SecurityConstants.TOKEN_LOGOUT)) {
String status = StringUtils.equals(SecurityConstants.AUTH_TOKEN, url) ? Constants.LOGIN_SUCCESS : Constants.LOGOUT;
String message = StringUtils.equals(SecurityConstants.AUTH_TOKEN, url) ? "登录成功" : "退出成功";
asyncService.recordLogininfor(username, status, message,request);
}
}
}
}
1.4.2 认证失败处理
/**
* 认证失败处理
*
*/
@Slf4j
@Component
public class AuthenticationFailureEvenHandler implements ApplicationListener<AbstractAuthenticationFailureEvent> {
@Autowired
private LogininforService asyncService;
@Override
public void onApplicationEvent(AbstractAuthenticationFailureEvent event) {
Authentication authentication = (Authentication) event.getSource();
if (StringUtils.isNotNull(authentication.getPrincipal())) {
HttpServletRequest request = ServletUtils.getRequest();
String url = request.getRequestURI();
String username = (String) authentication.getPrincipal();
log.info("用户:{} 认证失败,无法访问系统资源url:{}", username, url);
// 登录失败日志记录
if (StringUtils.equals(url, SecurityConstants.AUTH_TOKEN) && StringUtils.isNotNull(authentication.getDetails())) {
// 记录用户退出日志
asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"),request);
}
int code = HttpStatus.HTTP_UNAUTHORIZED;
String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
ServletUtils.renderString(ServletUtils.getResponse(), JsonUtils.toJsonString(AjaxResult.error(code, msg)));
}
}
}
1.4.3 自定义访问无权限资源时的异常
/**
* 自定义访问无权限资源时的异常
*
*/
@Slf4j
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
log.info("权限不足,请联系管理员 {}", request.getRequestURI());
int code = HttpStatus.FORBIDDEN;
String msg = accessDeniedException.getMessage();
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
}
}
1.4.4 自定义未登录处理
/**
* 自定义未登录处理
* AuthenticationEntryPoint是Spring Security Web一个概念模型接口,
* 顾名思义,他所建模的概念是:“认证入口点”。
* 它在用户请求处理过程中遇到认证异常时,
* 被ExceptionTranslationFilter用于开启特定认证方案(authentication schema)的认证流程。
*
*/
@Slf4j
@Component
public class CustomAuthExceptionEntryPoint implements AuthenticationEntryPoint {
@Autowired
private RedisClientDetailsService clientDetailsService;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
log.info("令牌不合法,禁止访问 {}", request.getRequestURI());
//获得请求参数
String clientId = request.getParameter(Oauth2Constants.CLIENT_ID);
if(StringUtils.isBlank(clientId)){
clientId = request.getParameter(Oauth2Constants.MY_CLIENT_ID);
}
// 查询客户端id 优先从redis缓存中获取,不存在则从数据库中获取
// 优先从redis缓存中获取,不存在则从数据库中获取
OauthClientDetails oauthClientDetails = RedisUtils.getCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientId);
if (StringUtils.isNull(oauthClientDetails)) {
LambdaQueryWrapper<OauthClientDetails> lqw = Wrappers.lambdaQuery();
oauthClientDetails = clientDetailsService.getOne(lqw.eq(OauthClientDetails::getAppKey, clientId));
if (StringUtils.isNull(oauthClientDetails)) {
ServletUtils.renderString(response, JSONObject.toJSONString(AjaxResult.error(HttpStatus.UNAUTHORIZED, "未识别应用",null)));
return;
}else {
RedisUtils.setCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientId, oauthClientDetails, oauthClientDetails.getAccessTokenValidity(), TimeUnit.SECONDS);
}
}
String param = StringUtils.formatUrlParamsWithAnnocation(oauthClientDetails);
String msg = authException.getMessage();
String authUrl = "http://localhost:8200/auth/login?" + param;
ServletUtils.renderString(response, JSONObject.toJSONString(AjaxResult.error(HttpStatus.UNAUTHORIZED, msg, authUrl)));
}
}
1.4.5 自定义退出处理类 返回成功
/**
* 自定义退出处理类 返回成功
*
*/
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
@Autowired
private TokenService tokenService;
@Autowired
private LogininforService asyncService;
/**
* 退出处理
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser)) {
String userName = loginUser.getUsername();
// 删除用户缓存记录
tokenService.delLoginUser(loginUser.getTokenKey());
// 记录用户退出日志
asyncService.recordLogininfor(userName, Constants.LOGOUT, "退出成功", request);
}
ServletUtils.renderString(response, JsonUtils.toJsonString(AjaxResult.error(HttpStatus.HTTP_OK, "退出成功")));
}
}
1.4.6 增加受信任的第三方处理
/**
* 重写当对于需要询问是否授权第三方应用时的方法,增加受信任的第三方处理
*/
@Slf4j
public class OauthUserApprovalHandler extends TokenStoreUserApprovalHandler {
/**
* 是否已批准授权(第三方应用)
*/
@Override
public boolean isApproved(AuthorizationRequest authorizationRequest, Authentication userAuthentication) {
if (super.isApproved(authorizationRequest, userAuthentication)) {
return true;
}
if (!userAuthentication.isAuthenticated()) {
return false;
}
RedisClientDetailsService oauthClientDetailsService = SpringUtils.getBean(RedisClientDetailsService.class);
ClientDetailsAdapter clientDetails = (ClientDetailsAdapter) oauthClientDetailsService.loadClientByClientId(authorizationRequest.getClientId());
return clientDetails != null && clientDetails.isTrusted();
}
}
1.5 自定义异常处理
/**
* OAuth2 自定义异常处理
*/
@Slf4j
public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator<OAuth2Exception> {
@Override
public ResponseEntity<OAuth2Exception> translate(Exception e) {
OAuth2Exception oAuth2Exception = (OAuth2Exception) e;
log.error("CustomWebResponseExceptionTranslator.status:{},oAuth2ErrorCode:{},message:{}",
oAuth2Exception.getHttpErrorCode(), oAuth2Exception.getOAuth2ErrorCode(), oAuth2Exception.getMessage());
return ResponseEntity.status(HttpServletResponse.SC_UNAUTHORIZED).body(oAuth2Exception);
}
}
1.6 常量
/**
* 缓存的key 常量
*
*/
public class Oauth2Constants {
/**
* 构建风格 1 elementui 2 antDesign
*/
public static final int IS_ELEMENTUI_MENU = 1;
//===========================================================
/**
* oauth 缓存前缀
*/
public static final String OAUTH_ACCESS = "oauth:access:";
/**
* oauth 客户端信息
*/
public static final String CLIENT_DETAILS_KEY = "oauth:client:details:";
/**
* 用户id
*/
public static final String CLIENT_USER_ID = "oauth:client:userId:";
/**
* token 请求头
*/
public static final String TOKEN_HEADER = "Authorization";
/**
* 客户端ID
*/
public static final String CLIENT_ID = "client_id";
/**
* 客户端ID
*/
public static final String MY_CLIENT_ID = "clientId";
/**
* redis 缓存的tokenkey
*/
public static final String TOKEN_KRY = "tokenKey";
/**
* 授权地址
*/
public static final String OAUTH2_ACCESS_TOKEN = "http://127.0.0.1:9500/oauth/token?client_id={clientId}" +
"&client_secret={clientSecret}&grant_type={grantType}&username={username}&password={password}";
/**
* 校验地址
*/
public static final String OAUTH2_CHECK_TOKEN = "http://127.0.0.1:9500/oauth/check_token?token={token}";
/**
* 刷新token地址
*/
public static final String OAUTH2_REFRESH_TOKEN = "http://127.0.0.1:9500/oauth/token?grant_type=refresh_token" +
"&refresh_token={token}&client_id={clientId}&client_secret={clientSecret}";
//======================================授权模式====================================
/**
* password 密码模式
*/
public static final String GRANT_TYPE_PASSWORD = "password";
/**
* authorization_code 授权码模式
*/
public static final String GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
/**
* client_credentials 客户端模式
*/
public static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
}
1.7 管理令牌
1.7 管理令牌方式
AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限。
自己可以创建 AuthorizationServerTokenServices 这个接口的实现,则需要继承 DefaultTokenServices 这个类,里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore :如其命名,所有的令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都实现了TokenStore接口:
InMemoryTokenStore :这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
JdbcTokenStore :这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到classpath当中。
JwtTokenStore :这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。
RedisTokenStore:
在使用Redis存储token,spring security oauth2 会生成以下几个key, 直接放出RedisTokenStore的源码吧
private static final String ACCESS = "access:";
private static final String AUTH_TO_ACCESS = "auth_to_access:";
private static final String AUTH = "auth:";
private static final String REFRESH_AUTH = "refresh_auth:";
private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
private static final String REFRESH = "refresh:";
private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
private static final String UNAME_TO_ACCESS = "uname_to_access:";
看一下RedisTokenStore的源码
//spring secuity oauth2提供的一个序列化工具
private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
//存储OAuth2AccessToken
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
byte[] serializedAccessToken = serialize(token);
......
}
//把存储对象序列化
private byte[] serialize(Object object) {
return serializationStrategy.serialize(object);
}
通过源码,我们发现spring security oauth2存储的是序列化后的对象,而不是json。(注意:应该是为提高存储效率,而不是加密操作)
那我们就以access:[AccessToken]这个key为例,在自己项目里写一个controller或者junit单元测试
@ApiOperation(value = "获取access存储内容", httpMethod = "GET")
@GetMapping("/deserialize/access")
public R<Object> deserializeAccessToken(@NotBlank(message = "accessToken不能为空"),
@RequestParam("accessToken") String accessToken) {
RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
byte[] serializedKey = serializationStrategy.serialize(Prefix.REDIS_SHIELD_OAUTH2 + "access:" + accessToken);
RedisConnection conn = connectionFactory.getConnection();
byte[] bytes;
try {
bytes = conn.get(serializedKey);
} finally {
conn.close();
}
OAuth2AccessToken content = serializationStrategy.deserialize(bytes, OAuth2AccessToken.class);
return RUtil.success(content);
}
结果如下
{
"access_token": "x8U6xmAK0MeFDEJ0",
"token_type": "bearer",
"refresh_token": "0qLDRZE70MeFDEI!",
"expires_in": 29658,
"scope": "server"
}
1.7.1 access:[AccessToken](对应对象:OAuth2AccessToken)
要如何获取这个key? 如果你已有spring security oauth2的项目(如果没有,可以github或spring官网找一个sample项目或自己搭建一个),可以类似这样发送一个请求:
http://localhost:6799/oauth/token?grant_type=password&username=wuji&password=12345678&client_id=app&client_secret=app
spring security oauth2获取AccessToken如果是密码授权方式(grant_type=password)除了携带用户名以及密码还要携带client_id和client_secret,当然,你也可以Http Basic方式将client_id和client_secret到请求头,如下
YXBwOmFwcA==是“client_id:client_secret” base64编码后的结果。 我们试着发送一个请求,返回结果如下:
{
//token,拿着这个token我们就可以资源(接口)了
"access_token": "x8U6xmAK0MeFDEJ0",
//token类型是一个票据类型,还有授权码类型等等
"token_type": "bearer",
//用与刷新access_token(资源访问token)的token
"refresh_token": "0qLDRZE70MeFDEI!",
//access_token剩余存活时间(单位是秒)
"expires_in": 29658,
//拿这个token可以访问那些范围内的资源
"scope": "server"
}
1.7.2 auth_to_access:[这里不再是AccessToken]
阅读源码,发现
public String extractKey(OAuth2Authentication authentication) {
//省略一些代码
values.put(USERNAME, authentication.getName());
values.put(CLIENT_ID, authorizationRequest.getClientId());
values.put(SCOPE, OAuth2Utils.formatParameterList(new TreeSet<String>(authorizationRequest.getScope())));
return generateKey(values);
}
auth_to_access:key, key是将username、client_id、scope三个值加密后的值,我们再看auth_to_access:存储的内容如下
{
"access_token": "x8U6xmAK0MeFDEJ0",
"token_type": "bearer",
"refresh_token": "0qLDRZE70MeFDEI!",
"expires_in": 24126,
"scope": "server"
}
跟access:的内容一模一样,那我们就可以知道,代码内部实现可以通过username、client_id、scope 3个字段获取AccessToken。
1.7.3 auth:[accessToken] (对应对象:OAuth2Authentication)
auth:存的内容如下:
{
"authorities": [{
"authority": "ROLE_USER"
},
{
"authority": "USER_RETRIEVE"
}
],
"details": null,
"authenticated": true,
"userAuthentication": {
"authorities": [{
"authority": "ROLE_USER"
},
{
"authority": "USER_RETRIEVE"
}
],
"details": {
"client_secret": "app",
"grant_type": "password",
"client_id": "app",
"username": "wuji"
},
"authenticated": true,
"principal": {
"password": null,
"username": "wuji",
"authorities": [{
"authority": "ROLE_USER"
},
{
"authority": "USER_RETRIEVE"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"credentials": null,
"name": "wuji"
},
"credentials": "",
"principal": {
"password": null,
"username": "wuji",
"authorities": [{
"authority": "ROLE_USER"
},
{
"authority": "USER_RETRIEVE"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"oauth2Request": {
"clientId": "app",
"scope": ["server"],
"requestParameters": {
"grant_type": "password",
"client_id": "app",
"username": "wuji"
},
"resourceIds": [],
"authorities": [],
"approved": true,
"refresh": false,
"redirectUri": null,
"responseTypes": [],
"extensions": {},
"grantType": "password",
"refreshTokenRequest": null
},
"clientOnly": false,
"name": "wuji"
}
主要包含当前登录用户的信息,以及用户附带的角色和和权限信息、生成Token时的授权方式等信息。
1.7.4 access_to_refresh:[accessToken]
access_to_refresh:存储的内容很简单,在通过password等授权方式获取token时的refreshToken
//refresh_token
grz0Xlzi0MeQwkx9
1.7.5 refresh_to_access:[refreshToken]
拿refreshToken去刷新accessToken时,会将新生成的accessToken放到refresh_to_access:
//access_token
TesxUOBt0MeRDDxA
1.7.6 refresh:[refreshToken]
存储内容如下:
{
//refresh_toekn
"value": "grz0Xlzi0MeQwkx9",
//过期时间戳(mills)
"expiration": 1557821765322
}
拿refreshToken去刷新accessToken时, 会先拿到这个KEY的信息,判断请求方的refresh token是否有效,无效的不能刷新access token。
1.7.7 refresh_auth:[refreshToken] (对应对象:OAuth2Authentication)
refresh_auth:存的内容与auth:类似,如下:
{
"authorities": [
{
"authority": "ROLE_USER"
},
{
"authority": "USER_RETRIEVE"
}
],
"details": null,
"authenticated": true,
"userAuthentication": {
"authorities": [
{
"authority": "ROLE_USER"
},
{
"authority": "USER_RETRIEVE"
}
],
"details": {
"client_secret": "app",
"grant_type": "password",
"client_id": "app",
"username": "wuji"
},
"authenticated": true,
"principal": {
"password": null,
"username": "wuji",
"authorities": [
{
"authority": "ROLE_USER"
},
{
"authority": "USER_RETRIEVE"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"credentials": null,
"name": "wuji"
},
"credentials": "",
"principal": {
"password": null,
"username": "wuji",
"authorities": [
{
"authority": "ROLE_USER"
},
{
"authority": "USER_RETRIEVE"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"oauth2Request": {
"clientId": "app",
"scope": ["server"],
"requestParameters": {
"grant_type": "password",
"client_id": "app",
"username": "wuji"
},
"resourceIds": [],
"authorities": [],
"approved": true,
"refresh": false,
"redirectUri": null,
"responseTypes": [],
"extensions": {},
"grantType": "password",
"refreshTokenRequest": null
},
"clientOnly": false,
"name": "wuji"
}
1.7.8 client_id_to_access:[client_id]
[{
"access_token": "TesxUOBt0MeRDDxA",
"token_type": "bearer",
"refresh_token": "grz0Xlzi0MeQwkx9",
"expires_in": 41714,
"scope": "server"
}]
顾名思义,这个key将对应client_id的AccessToken对象存储了起来,因为不同的username、scope使用同一个client_id去请求获取token,所以这是一个list。
1.7.9 uname_to_access:[client_id:username]
[{
"access_token": "TesxUOBt0MeRDDxA",
"token_type": "bearer",
"refresh_token": "grz0Xlzi0MeQwkx9",
"expires_in": 41714,
"scope": "server"
}]
这里也是一个list。
2. 客户端配置
2.1 application.yml 配置
security:
oauth2:
client:
# 客户端标识
client-id: test
# 客户端秘钥
client-secret: 123456
#地址为授权中心IP和端口
access-token-uri: http://127.0.0.1:9500/oauth/token
user-authorization-uri: http://127.0.0.1:9500/oauth/authorize
resource:
user-info-uri: http://127.0.0.1:9500/user
token-info-uri: http://127.0.0.1:9500/oauth/check_token
jwt:
key-uri: http://127.0.0.1:9500/oauth/token_key
2.2 pom依赖
<!-- JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
<!-- redis 缓存操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!-- spring security 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring security oauth2 开放授权 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
2.3 添加 @EnableOAuth2Sso
/**
* spring security配置
*
*/
@EnableOAuth2Sso
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
.....
}
@EnableOAuth2Sso注解详情
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableOAuth2Client
@EnableConfigurationProperties({OAuth2SsoProperties.class}) // [1]
@Import({OAuth2SsoDefaultConfiguration.class, OAuth2SsoCustomConfiguration.class, ResourceServerTokenServicesConfiguration.class})
public @interface EnableOAuth2Sso {
}
由源码知@EnableOAuth2Sso是一个组合注解。
@EnableOAuth2Client
@EnableOAuth2Client通过@Import导入了OAuth2ClientConfiguration类。
OAuth2ClientConfiguration类中注入了3个Bean:OAuth2ClientContextFilter,AccessTokenRequest和OAuth2ClientContextConfiguration。
OAuth2ClientContextFilter加入到过滤器链中,当后续的认证抛出UserRedirectRequiredException异常时,会重定向。。。
@EnableConfigurationProperties
@EnableConfigurationProperties启用OAuth2SsoProperties配置类。 回调 /login
security.oauth2.sso.loginPath=/login
@Import
导入了OAuth2SsoDefaultConfiguration,OAuth2SsoCustomConfiguration和ResourceServerTokenServicesConfiguration三个配置类。
看OAuth2SsoDefaultConfiguration的条件@Conditional({OAuth2SsoDefaultConfiguration.NeedsWebSecurityCondition.class})和OAuth2SsoCustomConfiguration上的条件@Conditional({EnableOAuth2SsoCondition.class}),它们两个是取反的逻辑,即只能有一个生效。
@Conditional({EnableOAuth2SsoCondition.class})是指当项目中有WebSecurityConfigurerAdapter.class的Bean时为真,即项目中继承了WebSecurityConfigurerAdapter抽象类。
① OAuth2SsoDefaultConfiguration配置类
OAuth2SsoDefaultConfiguration继承了WebSecurityConfigurerAdapter,重写了config(HttpSecurity)方法
protected void configure(HttpSecurity http) throws Exception {
((AuthorizedUrl)http.antMatcher("/**").authorizeRequests().anyRequest()).authenticated();
(new SsoSecurityConfigurer(this.applicationContext)).configure(http);
}
方法中配置了所有的请求都需要认证后访问。另外,调用了SsoSecurityConfigurer的config方法。
SsoSecurityConfigurer的configure
public void configure(HttpSecurity http) throws Exception {
// 在[1]处已经启用了OAuth2SsoProperties属性类
OAuth2SsoProperties sso = (OAuth2SsoProperties)this.applicationContext.getBean(OAuth2SsoProperties.class);
// oauth2SsoFilter方法返回OAuth2ClientAuthenticationProcessingFilter对象
// OAuth2ClientAuthenticationConfigurer主要作用将OAuth2ClientAuthenticationProcessingFilter添加到过滤器链中
http.apply(new SsoSecurityConfigurer.OAuth2ClientAuthenticationConfigurer(this.oauth2SsoFilter(sso)));
// 认证失败的异常处理端点
this.addAuthenticationEntryPoint(http, sso);
}
SsoSecurityConfigurer的oauth2SsoFilter
private OAuth2ClientAuthenticationProcessingFilter oauth2SsoFilter(OAuth2SsoProperties sso) {
OAuth2RestOperations restTemplate = ((UserInfoRestTemplateFactory)this.applicationContext.getBean(UserInfoRestTemplateFactory.class)).getUserInfoRestTemplate();
ResourceServerTokenServices tokenServices = (ResourceServerTokenServices)this.applicationContext.getBean(ResourceServerTokenServices.class);
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(sso.getLoginPath());
filter.setRestTemplate(restTemplate);
filter.setTokenServices(tokenServices);
filter.setApplicationEventPublisher(this.applicationContext);
return filter;
}
方法返回OAuth2ClientAuthenticationProcessingFilter对象,OAuth2ClientAuthenticationProcessingFilter与UsernamePasswordAuthenticationFilter一样处理用户认证的逻辑。不同的是UsernamePasswordAuthenticationFilter通过username和password去认证了,而OAuth2ClientAuthenticationProcessingFilter是通过OAuth2RestOperations「restTemplate」取获取accessToken,然后使用accessToken获取认证信息。
SsoSecurityConfigurer.OAuth2ClientAuthenticationConfigurer类
private static class OAuth2ClientAuthenticationConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private OAuth2ClientAuthenticationProcessingFilter filter;
OAuth2ClientAuthenticationConfigurer(OAuth2ClientAuthenticationProcessingFilter filter) {
this.filter = filter;
}
public void configure(HttpSecurity builder) throws Exception {
OAuth2ClientAuthenticationProcessingFilter ssoFilter = this.filter;
ssoFilter.setSessionAuthenticationStrategy((SessionAuthenticationStrategy)builder.getSharedObject(SessionAuthenticationStrategy.class));
// 将OAuth2ClientAuthenticationProcessingFilter添加到过滤器链中
builder.addFilterAfter(ssoFilter, AbstractPreAuthenticatedProcessingFilter.class);
}
}
该类的config方法中为OAuth2ClientAuthenticationProcessingFilter设置了Session策略,然后将其添加到过滤器链中。
② OAuth2SsoCustomConfiguration配置类
该类OAuth2SsoCustomConfiguration作用是:如果项目中实现了WebSecurityConfigurerAdapter的配置类,则为该类使用代理。
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (this.configType.isAssignableFrom(bean.getClass()) && bean instanceof WebSecurityConfigurerAdapter) {
// 使用代理
ProxyFactory factory = new ProxyFactory();
factory.setTarget(bean);
factory.addAdvice(new OAuth2SsoCustomConfiguration.SsoSecurityAdapter(this.applicationContext));
bean = factory.getProxy();
}
return bean;
}
SsoSecurityAdapter
private static class SsoSecurityAdapter implements MethodInterceptor {
private SsoSecurityConfigurer configurer;
SsoSecurityAdapter(ApplicationContext applicationContext) {
this.configurer = new SsoSecurityConfigurer(applicationContext);
}
public Object invoke(MethodInvocation invocation) throws Throwable {
if (invocation.getMethod().getName().equals("init")) {
// 在调用init方法添加SsoSecurityConfigurer配置类
Method method = ReflectionUtils.findMethod(WebSecurityConfigurerAdapter.class, "getHttp");
ReflectionUtils.makeAccessible(method);
HttpSecurity http = (HttpSecurity)ReflectionUtils.invokeMethod(method, invocation.getThis());
this.configurer.configure(http);
}
return invocation.proceed();
}
}
ResourceServerTokenServicesConfiguration
这个配置类主要是向容器中添加了UserInfoRestTemplateFactory和JwtToken相关的配置,被用于获取用户信息及token的生成与存储。
2.4 校验令牌方法
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token)) {
try {
//调用授权中心接口进行校验
Map<String, String> map = new HashMap<>(1);
map.put("token",token);
ResponseEntity<AjaxResult> response = RestTemplateUtils.post(tokenProperties.getCheckTokenUrl(),map, AjaxResult.class);
if (response.getStatusCodeValue() == HttpStatus.HTTP_OK) {
if(StringUtils.isNotEmpty(response.getBody())){
AjaxResult result = response.getBody();
OauthCallBackInfo oauthCallBackInfo = JSONObject.parseObject(result.getData().toString(), OauthCallBackInfo.class);
//可以对用户信息进行缓存 是否缓存?
return oauthCallBackInfo.getLoginUser();
}
}else {
throw new BaseException(response.getBody().toString());
}
} catch (Exception e) {
log.error("获取身份信息失败!" + e.getMessage());
}
}
return null;
}
/**
* 获取请求token
*
* @return token
*/
public String getToken(HttpServletRequest request) {
String token = request.getHeader(tokenProperties.getHeader());
// Constants.TOKEN_PREFIX = "Bearer "
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
/**
* 授权回调信息
* @date 2022/4/7 23:56
*/
@Data
public class OauthCallBackInfo {
/**
* 过期时间
*/
private String expire;
/**
* info
*/
private List<Map<String, Object>> info;
/**
* 登录人信息
*/
private LoginUser loginUser;
}
|