Spring security Oauth2
本文最后更新于 2023-12-09,文章内容可能已经过时。
Spring Security Oauth2:
Spring Security OAuth2 是 Spring Security 提供的一个子项目,用于支持 OAuth 2.0 认证协议。OAuth 2.0 是一种开放标准的授权协议,广泛用于在不暴露用户凭据的情况下,允许第三方应用程序访问用户资源。
1.依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
2.配置security:
这是spring security的配置类,主要有两个功能,一是配置要拦截的接口,二是配置provider,每个provider代表一个授权者.
package com.elevator.auth.security.config;
import com.elevator.auth.security.extension.idNumPwd.IdNumPasswordAuthenticationProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 安全配置
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService sysUserDetailsService;
private final StringRedisTemplate redisTemplate;
//HttpSecurity配置
//我们可以通过它来控制接口的安全访问策略
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 指定的接口直接放行
.antMatchers("/oauth/**").permitAll()
// @link https://gitee.com/xiaoym/knife4j/issues/I1Q5X6 (接口文档knife4j需要放行的规则)
.antMatchers("/webjars/**", "/doc.html", "/swagger-resources/**", "/v3/api-docs", "/swagger-ui/**").permitAll()
// 其他的接口都需要认证后才能请求
.anyRequest().authenticated()
.and()
// 禁用 CSRF
.csrf().disable();
}
/**
* 认证管理对象
*
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//注册了多个provider,用于在不同模式,获取在数据库内的用户信息并作认证
@Override
public void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(idNumPasswordAuthenticationProvider())
.authenticationProvider(daoAuthenticationProvider());
}
/**
* 身份证号密码认证授权提供者
*
* @return
*/
@Bean
public IdNumPasswordAuthenticationProvider idNumPasswordAuthenticationProvider() {
IdNumPasswordAuthenticationProvider provider = new IdNumPasswordAuthenticationProvider();
// SysUserServiceImpl实现UserDetailsService接口的子类UserService,复写了loadUserByUsername方法
provider.setUserDetailsService(sysUserDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
/**
* 用户名密码认证授权提供者
*
* @return
*/
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(sysUserDetailsService);
provider.setPasswordEncoder(passwordEncoder());
provider.setHideUserNotFoundExceptions(false); // 是否隐藏用户不存在异常,默认:true-隐藏;false-抛出异常;
return provider;
}
/**
* 密码编码器
* <p>
* 委托方式,根据密码的前缀选择对应的encoder,例如:{bcypt}前缀->标识BCYPT算法加密;{noop}->标识不使用任何加密即明文的方式
* 密码判读 IdNumPasswordAuthenticationProvider
*/
@Bean
PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
3.配置AuthorizationServerConfig:授权服务器:
主要对oauth的端点进行配置.
/**
* @author Dave Syer
*
*/
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
/**
* 用来配置令牌端点的安全约束
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
/**
* 用来配置客户端信息服务,客户端详情信息在这里初始化,可以写死在代码里,也可以放到配置文件或者数据库
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
/**
* 用来配置令牌的访问端点和令牌服务
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
}
这是配置示例:
package com.elevator.auth.security.config;
import com.alibaba.druid.support.json.JSONUtils;
import com.elevator.auth.security.detail.UserDetail;
import com.elevator.auth.security.extension.idNumPwd.IdNumPasswordTokenGranter;
import com.elevator.auth.security.extension.refresh.PreAuthenticatedUserDetailsService;
import com.elevator.auth.security.service.ClientDetailsServiceImpl;
import com.elevator.auth.security.service.SysUserServiceImpl;
import com.elevator.common.constants.SecurityConstant;
import com.elevator.common.response.Response;
import com.elevator.common.response.ResponseCode;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
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.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import java.security.KeyPair;
import java.util.*;
/**
* OAuth 认证授权配置
*/
@Configuration
@EnableAuthorizationServer
@RequiredArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private ClientDetailsServiceImpl clientDetailsService;
@Autowired
private SysUserServiceImpl sysUserDetailsService;
/**
* jks文件
*/
@Value("${jks.file}")
private String jksFile;
/**
* jks密码
*/
@Value("${jks.password}")
private String jksPassword;
/**
* OAuth2客户端
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//自定义ClientDetailsService
clients.withClientDetails(clientDetailsService);
}
/**
* 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
// Token增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
tokenEnhancers.add(tokenEnhancer());
tokenEnhancers.add(jwtAccessTokenConverter());
tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
//token存储模式设定 默认为InMemoryTokenStore模式存储到内存中
endpoints.tokenStore(jwtTokenStore());
// 获取原有默认授权模式(授权码模式、密码模式、客户端模式、简化模式)的授权者
List<TokenGranter> granterList = new ArrayList<>(Arrays.asList(endpoints.getTokenGranter()));
//添加密码授权模式的授权者
granterList.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(),
endpoints.getOAuth2RequestFactory()
));
// 添加身份证号授权模式的授权者
granterList.add(new IdNumPasswordTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory(),
this.authenticationManager));
CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(granterList);
endpoints
.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.tokenEnhancer(tokenEnhancerChain)
.tokenGranter(compositeTokenGranter)
.tokenServices(tokenServices(endpoints))
;
}
/**
* jwt token存储模式
*/
@Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
public DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
tokenEnhancers.add(tokenEnhancer());
tokenEnhancers.add(jwtAccessTokenConverter());
tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setTokenEnhancer(tokenEnhancerChain);
// 多用户体系下,刷新token再次认证客户端ID和 UserDetailService 的映射Map
Map<String, UserDetailsService> clientUserDetailsServiceMap = new HashMap<>();
clientUserDetailsServiceMap.put(SecurityConstant.ADMIN_WEB_CLIENT_ID, sysUserDetailsService); // 系统管理web客户端
clientUserDetailsServiceMap.put(SecurityConstant.ADMIN_APP_CLIENT_ID, sysUserDetailsService); // 系统管理移动客户端
clientUserDetailsServiceMap.put(SecurityConstant.MAINTAIN_CLIENT_ID, sysUserDetailsService); // 维保app客户端
clientUserDetailsServiceMap.put(SecurityConstant.USE_CLIENT_ID, sysUserDetailsService); // 物业app客户端
// 刷新token模式下,重写预认证提供者替换其AuthenticationManager,可自定义根据客户端ID和认证方式区分用户体系获取认证用户信息
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new PreAuthenticatedUserDetailsService<>(clientUserDetailsServiceMap));
tokenServices.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));
/** refresh_token有两种使用方式:重复使用(true)、非重复使用(false),默认为true
* 1 重复使用:access_token过期刷新时, refresh_token过期时间未改变,仍以初次生成的时间为准
* 2 非重复使用:access_token过期刷新时, refresh_token过期时间延续,在refresh_token有效期内刷新便永不失效达到无需再次登录的目的
*/
tokenServices.setReuseRefreshToken(true);
return tokenServices;
}
/**
* 使用对称加密算法对token签名
*/
// @Bean
// public JwtAccessTokenConverter jwtAccessTokenConverter() {
// JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
// accessTokenConverter.setSigningKey("key"); //对称加密key
// return accessTokenConverter;
// }
/**
* 使用非对称加密算法对token签名
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyPair());
return converter;
}
/**
* 密钥库中获取密钥对(公钥+私钥)
*/
@Bean
public KeyPair keyPair() {
return new KeyStoreKeyFactory(
new ClassPathResource(jksFile), jksPassword.toCharArray())
.getKeyPair("jwt");
}
/**
* JWT内容增强
*/
@Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
Map<String, Object> additionalInfo = new HashMap<>();
Object principal = authentication.getUserAuthentication().getPrincipal();
if (principal instanceof UserDetail) {
UserDetail userDetails = (UserDetail) principal;
additionalInfo.put("id", userDetails.getId());
additionalInfo.put("username", userDetails.getUsername());
additionalInfo.put("idNum", userDetails.getIdNum());
additionalInfo.put("unitId",userDetails.getUnitId());
additionalInfo.put("unitType",userDetails.getUnitType());
additionalInfo.put("roleList",userDetails.getAuthorities());
additionalInfo.put("nickName",userDetails.getNickName());
} else {
}
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
};
}
/**
* 自定义认证异常响应数据
*/
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return (request, response, e) -> {
response.setStatus(HttpStatus.OK.value());
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control", "no-cache");
Response<Object> resp = Response.failed(ResponseCode.CLIENT_AUTHENTICATION_FAILED);
response.getWriter().print(JSONUtils.toJSONString(resp));
response.getWriter().flush();
};
}
}
4.配置登陆端点:
@ApiOperation(value = "OAuth2认证", notes = "登录入口")
@PostMapping("/token")
public Object postAccessToken(
Principal principal,
@RequestParam Map<String, String> parameters
) throws HttpRequestMethodNotSupportedException {
//用户名密码登录时,md5加密的密码怎么办?
/**
* 获取登录认证的客户端ID
* 放在请求头(Request Headers)中的Authorization字段,且经过加密,例如 Basic Y2xpZW50OnNlY3JldA== 明文等于 client:secret
*/
String clientId = RequestUtils.getOAuth2ClientId();
log.info("OAuth认证授权 客户端ID:{},请求参数:{}", clientId, JSONUtils.toJSONString(parameters));
OAuth2AccessToken accessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
if(!Objects.equals(parameters.get("grant_type"), "refresh_token")){
String unitType = parameters.get("unitType");
if(!unitType.equals(accessToken.getAdditionalInformation().get("unitType").toString())){
return Response.failed("单位类型对应错误");
}
}
5.配置资源服务器:
资源服务器主要负责验证token安全性,解析token,获取相关的权限信息.
package com.elevator.gateway.security;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.elevator.common.constants.SecurityConstant;
import com.nimbusds.jose.JWSObject;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 网关自定义鉴权管理器
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class ResourceServerManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private final RedisTemplate redisTemplate;
@SneakyThrows
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
if (request.getMethod() == HttpMethod.OPTIONS) { // 预检请求放行
return Mono.just(new AuthorizationDecision(true));
}
PathMatcher pathMatcher = new AntPathMatcher(); // 【声明定义】Ant路径匹配模式,“请求路径”和缓存中权限规则的“URL权限标识”匹配
String method = request.getMethodValue();
String path = request.getURI().getPath();
String restfulPath = method + " " + path;
// 如果token以"bearer "为前缀,到此方法里说明JWT有效即已认证
String token = request.getHeaders().getFirst(SecurityConstant.AUTHORIZATION_KEY);
if (StrUtil.isEmpty(token) || !StrUtil.startWithIgnoreCase(token, SecurityConstant.JWT_PREFIX) ) {
return Mono.just(new AuthorizationDecision(false));
}
// 解析JWT
token = StrUtil.replaceIgnoreCase(token, SecurityConstant.JWT_PREFIX, Strings.EMPTY);
String payload = StrUtil.toString(JWSObject.parse(token).getPayload());
JSONObject jsonObject = JSONUtil.parseObj(payload);
// 获取 ClientId
String clientId = jsonObject.getStr(SecurityConstant.CLIENT_ID_KEY);
/**
* 鉴权开始
* 缓存取 [URL权限-角色权限集合] 规则数据
* urlPermRolesRules = [{'key':'GET /admin/user/*','value':['role1', 'role2']},...]
*/
// 判断是否存在权限对应数据
if (!redisTemplate.hasKey(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY)) {
return Mono.just(new AuthorizationDecision(false));
}
if (!redisTemplate.hasKey(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY)) {
return Mono.just(new AuthorizationDecision(false));
}
if (!redisTemplate.hasKey(SecurityConstant.USE_APP_URL_PERM_ROLES_KEY)) {
return Mono.just(new AuthorizationDecision(false));
}
if (!redisTemplate.hasKey(SecurityConstant.MAINTAIN_APP_URL_PERM_ROLES_KEY)) {
return Mono.just(new AuthorizationDecision(false));
}
Map<String, Object> webUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY);
Set<String> webUrlPermsSet = webUrlPermRolesRules.keySet();
Map<String, Object> appUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY);
Set<String> appUrlPermsSet = appUrlPermRolesRules.keySet();
Map<String, Object> useUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.USE_APP_URL_PERM_ROLES_KEY);
Set<String> useUrlPermsSet = useUrlPermRolesRules.keySet();
Map<String, Object> maintainUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.MAINTAIN_APP_URL_PERM_ROLES_KEY);
Set<String> maintainUrlPermsSet = maintainUrlPermRolesRules.keySet();
// 判断端
Map<String, Object> urlPermRolesRules = null;
if (SecurityConstant.ADMIN_WEB_CLIENT_ID.equals(clientId)) {
urlPermRolesRules = webUrlPermRolesRules;
// 如果访问的接口路径在web端没有,禁止访问
if (!webUrlPermsSet.contains(restfulPath)) {
return Mono.just(new AuthorizationDecision(false));
}
} else if (SecurityConstant.ADMIN_APP_CLIENT_ID.equals(clientId)) {
urlPermRolesRules = appUrlPermRolesRules;
// 如果访问的接口路径在app端没有,禁止访问
if (!appUrlPermsSet.contains(restfulPath)) {
return Mono.just(new AuthorizationDecision(false));
}
}else if(SecurityConstant.USE_CLIENT_ID.equals(clientId)){
urlPermRolesRules = useUrlPermRolesRules;
// 如果访问的接口路径在使用app端没有,禁止访问
if (!useUrlPermsSet.contains(restfulPath)) {
return Mono.just(new AuthorizationDecision(false));
}
}else if(SecurityConstant.MAINTAIN_CLIENT_ID.equals(clientId)){
urlPermRolesRules = maintainUrlPermRolesRules;
// 如果访问的接口路径在维保app端没有,禁止访问
if (!maintainUrlPermsSet.contains(restfulPath)) {
return Mono.just(new AuthorizationDecision(false));
}
}
// 根据请求路径获取有访问权限的角色列表
List<String> authorizedRoles = new ArrayList<>(); // 拥有访问权限的角色
boolean requireCheck = false; // 是否需要鉴权,默认未设置拦截规则不需鉴权
for (Map.Entry<String, Object> permRoles : urlPermRolesRules.entrySet()) {
String perm = permRoles.getKey();
if (pathMatcher.match(perm, restfulPath)) {
List<String> roles = Convert.toList(String.class, permRoles.getValue());
authorizedRoles.addAll(roles);
if (!requireCheck) {
requireCheck = true;
}
}
}
// 没有设置拦截规则放行
if (requireCheck == false) {
return Mono.just(new AuthorizationDecision(true));
}
// 判断JWT中携带的用户角色权限是否有权限访问
Mono<AuthorizationDecision> authorizationDecisionMono = mono
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authority -> {
String permCode = StrUtil.removePrefix(authority, SecurityConstant.AUTHORITY_PREFIX);
boolean hasAuthorized = CollectionUtil.isNotEmpty(authorizedRoles) && authorizedRoles.contains(permCode);
return hasAuthorized;
})
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
return authorizationDecisionMono;
}
}
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果