Auth认证服务
1、AuthorizationServerConfig
【事先准备】:
方法、LoadRolePermissionService 调用—> PermissionServiceImpl
作用: 从数据库中将url->角色对应关系加载到Redis中
- **方法1: listRolePermission **
-
- 先从数据库获取permissons
- 根据roleid从数据库中找权限数据
- 根据permissionid从数据库中找数据
- 到此为止,构造了每一个permission对应所需要的权限一共后续使用
- 简化一下,放入redis
【1】AuthorizationServerConfig 继承—> AuthorizationServerConfigurerAdapter
作用: 配置认证中心,就是授权服务器配置
方法1: configure(ClientDetailsServiceConfigurer clients)
/**
* 配置客户端详情,并不是所有的客户端都能接入授权服务
* 用来配置客户端详情服务(ClientDetailsService),
* 客户端详情信息在这里进行初始化,
* 你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息
*/
一、重写configure(ClientDetailsServiceConfigurer clients)方法主要配置客户端,就是告诉auth服务,我有什么客户,每一个客户又是什么样子的,
- 定义两个client_id,及客户端可以通过不同的client_id来获取不同的令牌;
- client_id为test1的令牌有效时间为3600秒,client_id为test2的令牌有效时间为7200秒;
- client_id为test1的refresh_token(下面会介绍到)有效时间为864000秒,即10天,也就是说在这10天内都可以通过refresh_token来换取新的令牌;
- 在获取client_id为test1的令牌的时候,scope只能指定为all,a,b或c中的某个值,否则将获取失败;
- 只能通过密码模式(password)来获取client_id为test1的令牌,而test2则无限制。
二、导入方法有以下几种:
- 内存,自己测试的时候比较推荐,后续数据库啊什么的
- 数据库,使用JdbcClientDetailsService,JdbcClientDetailsService自己是有一个默认的字段的表的,所以程序是从数据库中的oauth_client_details表中加载客户端信息,
- 总而言之,就是配置把客户端信息从数据源拿过来。后面需要授权验证,要用到,所以先配。
方法2: configure(AuthorizationServerEndpointsConfigurer endpoints)------
上面和客户端有关,而这个则直接和令牌有关,比如配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services),还有一些其他的,比如异常啊什么的,下面有例子。
/**
* 配置令牌访问的端点
*/
令牌端点可用于以编程方式请求令牌(非常重要,四种方式),下面是配置的例子
endpoints
//设置异常WebResponseExceptionTranslator,用于处理用户名,密码错误、授权类型不正确的异常
.exceptionTranslator(new OAuthServerWebResponseExceptionTranslator())
//授权码模式所需要的authorizationCodeServices
.authorizationCodeServices(authorizationCodeServices())
//密码模式所需要的authenticationManager
.authenticationManager(authenticationManager)
//令牌管理服务,无论哪种模式都需要
.tokenServices(tokenServices())
//添加进入tokenGranter
.tokenGranter(new CompositeTokenGranter(tokenGranters))
//只允许POST提交访问令牌,uri:/oauth/token
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
一、authenticationManager (建议必须配置) (密码授权管理器),见文件SecurityConfig这个配置类
- 在Spring Security中,AuthenticationManager的默认实现是ProviderManager,而且它不直接自己处理认证请求,而是委托给其所配置的AuthenticationProvider列表,然后会依次使用每一个AuthenticationProvider进行认证,如果有一个AuthenticationProvider认证后的结果不为null,则表示该AuthenticationProvider已经认证成功,之后的AuthenticationProvider将不再继续认证。然后直接以该AuthenticationProvider的认证结果作为ProviderManager的认证结果。如果所有的AuthenticationProvider的认证结果都为null,则表示认证失败,将抛出一个ProviderNotFoundException。
校验认证请求最常用的方法是根据请求的用户名加载对应的UserDetails,然后比对UserDetails的密码与认证请求的密码是否一致,一致则表示认证通过。
Spring Security内部的DaoAuthenticationProvider就是使用的这种方式。其内部使用UserDetailsService来负责加载UserDetails。在认证成功以后会使用加载的UserDetails来封装要返回的Authentication对象,加载的UserDetails对象是包含用户权限等信息的。认证成功返回的Authentication对象将会保存在当前的SecurityContext中
二、令牌本身内容的配置(建议必须配置)
I、 两种方式,
【1】直接在endpoint这里配。
【2】自己写个bean注入,举个例子:
II、tokenServices() 就在本文件注入,由于是授权服务,所以涉及到颁发令牌,那么有关令牌的管理,比如过期时间,是jwt还是什么格式,客户端存储策略,都在这里
@Bean
public AuthorizationServerTokenServices tokenServices() {
System.out.println("令牌管理服务的配置");
DefaultTokenServices services = new DefaultTokenServices();
//客户端端配置策略
services.setClientDetailsService(clientDetailsService);
//支持令牌的刷新
services.setSupportRefreshToken(true);
//令牌服务
services.setTokenStore(tokenStore);
//access_token的过期时间
services.setAccessTokenValiditySeconds(60 * 60 * 24 * 3);
//refresh_token的过期时间
services.setRefreshTokenValiditySeconds(60 * 60 * 24 * 3);
//设置令牌增强,使用JwtAccessTokenConverter进行转换
services.setTokenEnhancer(jwtAccessTokenConverter);
return services;
}
III、根据项目的要求去选择令牌的内容配置,一般来说都是jwt或者jwt+自定义内容。
-
把令牌变成jwt格式很简单,按下面2两步操作即可,反正就是
-
@Configuration public class JWTokenConfig { @Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("test_key"); // 签名密钥 return accessTokenConverter; } }
-
services.setTokenStore(tokenStore);
-
-
自定义的话,同样简单,按下面的操作来做,无非就是先写一些配置,然后把配置注入adapter
-
@Component public class JwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); Map<String, Object> info = new HashMap<>(); //把用户ID设置到JWT中 info.put("id", securityUser.getId()); info.put("client_id",securityUser.getClientId()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); return accessToken; } }
-
tokenEnhancer(enhancerChain)
-
三、tokenGranter(new CompositeTokenGranter(tokenGranters)) 自定义授权 非常重要
作用:自定义授权获取token,下面我们来看一看源码是怎么获取token的,在我们发起oauth/token,请求获取token时,实际上是请求Tokenpoint类的postAccessToken或者getacesstoken方法,相当于调用了一个controller方法,根据请求的方法是get还是post,但其实内部还是调用post的方法。
在TokenEndPoint 获取令牌过程中, 有个这样的步骤:
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
postAccessToken这个方法中,在这个方法的132行调用TokenGranter类的grant方法来获取token,这个方法也是最重要的,通过这个方法我们可以对请求的参数进行校验是否合法,是否给予令牌。
TokenGranter是一个接口,它有多个实现类,CompositeTokenGranter是其中之一,在grant方法中,会循环遍历所有的授权方式,根据请求参数携带的授权方式码,来匹配对应的授权处理实现类,调用实现类中的grant方法。那么关键点来了,请求参数中携带的是我们**自定义的授权方式码**,如果要匹配上,那么首先我们要创建自定义的授权处理类,然后把这个授权处理类放入Spring Security默认的授权处理集合中,这样才能循环匹配上,进行下一步。
和以前的做法一样:创建自定义授权处理类,我们可以继承TokenGranter来实现自定义的身份验证以便获取token,而AbstractTokenGranter是一个继承TokenGranter的实现类,一般我们都会继承这个类进行使用。这一点已经得到验证,可以看下面的流程。
实现类 | 对应的授权模式 |
---|---|
AuthorizationCodeTokenGranter | 授权码模式 |
ClientCredentialsTokenGranter | 客户端模式 |
ImplicitTokenGranter | implicit 模式 |
RefreshTokenGranter | 刷新 token 模式 |
ResourceOwnerPasswordTokenGranter | 密码模式 |
这些类都继承了AbstractTokenGranter
AbstractTokenGranter 调用------->getAccessToken -------->getOAuth2Authentication
根据 client、tokenRequest 从 OAuth2RequestFactory 中创建一个 OAuth2Request, 进而可得到 OAuth2Authentication (存放着用户的认证信息)。
通过 tokenService 去创建 OAuth2AccessToken (存放着用户的 token信息、过期时间)。
I、————所以,这里加入自定的tokenGrant,也就是要自定义自己的授权方法 去 授权 自定义的令牌——————
自定义至关重要的一点就是修改getOAuth2Authentication方法 (主要文件见sms文件夹和即可)
具体修改不讲,修改的流程就是1、组装自定义模式的认证信息 2、用authenticationManager去调用内部自定义的Provider认证这个认证信息,认证规则自然是写在Provider里面。可借鉴 https://blog.csdn.net/m0_38031406/article/details/89316342
II、所以实现方式:继承AbstractTokenGranter + 重写 getOAuth2Authentication方法。
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
String mobile = parameters.get("mobile");
String password = parameters.get("password");
//将其中的密码移除
parameters.remove("password");
//自定义的token类
Authentication userAuth = new MobilePasswordAuthenticationToken(mobile, password);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
//调用AuthenticationManager进行认证,内部会根据MobileAuthenticationToken找到对应的Provider进行认证
userAuth = authenticationManager.authenticate(userAuth);
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate mobile: " + mobile);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
这里调用了AuthenticationManager认证,后面回调用自定义的XXXXXAuthenticationProvider
III、注入
.tokenGranter(new CompositeTokenGranter(tokenGranters))
四、new OAuthServerWebResponseExceptionTranslator() 也是自己配置的见文件夹Exception
自定义异常翻译器,针对用户名、密码异常,授权类型不支持的异常进行处理-----关键是用户
五、authorizationCodeServices()就在本文件注入
六、待续。。。。。。
方法3: configure(AuthorizationServerSecurityConfigurer security)
/**
* 配置令牌访问的安全约束()
*/
一、配置OAuthServerClientCredentialsTokenEndpointFilter------主要是客户端的验证
/**
* @author 客户端异常处理
* 自定义的客户端认证的过滤器,根据客户端的id、秘钥进行认证
* 重写这个过滤器用于自定义异常处理
* 具体认证的逻辑依然使用ClientCredentialsTokenEndpointFilter,只是设置一下AuthenticationEntryPoint为定制
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
System.out.println("配置令牌访问的安全约束");
//自定义ClientCredentialsTokenEndpointFilter,用于处理客户端id,密码错误的异常
OAuthServerClientCredentialsTokenEndpointFilter endpointFilter = new OAuthServerClientCredentialsTokenEndpointFilter(security,authenticationEntryPoint);
endpointFilter.afterPropertiesSet();
security.addTokenEndpointAuthenticationFilter(endpointFilter);
security
.authenticationEntryPoint(authenticationEntryPoint)
//开启/oauth/token_key验证端口权限访问
.tokenKeyAccess("permitAll()")
//开启/oauth/check_token验证端口认证权限访问
.checkTokenAccess("permitAll()");
//一定不要添加allowFormAuthenticationForClients,否则自定义的OAuthServerClientCredentialsTokenEndpointFilter不生效
// .allowFormAuthenticationForClients();
}
}
I、具体认证的逻辑依然使用ClientCredentialsTokenEndpointFilter,只是设置一下AuthenticationEntryPoint为定制
既然如此,我们就去看自定义的AuthenticationEntryPoint,这里没改,实际中可以改
public class OAuthServerAuthenticationEntryPoint implements AuthenticationEntryPoint {
/**
* 认证失败处理器会调用这个方法返回提示信息
* TODO 实际开发中可以自己定义,此处直接返回JSON数据:客户端认证失败错误提示
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
ResponseUtils.result(response,new ResultMsg(ResultCode.CLIENT_AUTHENTICATION_FAILED.getCode(),ResultCode.CLIENT_AUTHENTICATION_FAILED.getMsg(),null));
}
}
II、endpointFilter.afterPropertiesSet(); 认证成功怎么办,认真失败怎么办,这里可以自定义哈哈
/**
* 设置AuthenticationEntryPoint主要逻辑
*/
@Override
public void afterPropertiesSet() {
System.out.println("设置AuthenticationEntryPoint主要逻辑");
//TODO 定制认证失败处理器,开发中可以自己修改
setAuthenticationFailureHandler((request, response, exception) -> {
if (exception instanceof BadCredentialsException) {
exception = new BadCredentialsException(exception.getMessage(), new BadClientCredentialsException());
}
authenticationEntryPoint.commence(request, response, exception);
});
//成功处理器,和父类相同,为空即可。
setAuthenticationSuccessHandler((request, response, authentication) -> {
});
}
III、security.addTokenEndpointAuthenticationFilter(endpointFilter);
- 注入 自定义相应异常的过滤链
2、springconfig
介绍一个比较完整的securityconfig配置
@Configuration
//开启判断用户对某个控制层的方法是否具有访问权限的功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//注入自定义的UserDetailService
@Autowired
@Lazy
private UserDetailsServiceImpl userDetailsServiceImpl;
@Autowired
private StringRedisTemplate stringRedisTemplate;
//替换默认AuthenticationManager中的UserDetailService,使用数据库用户认证方式登录
//1. 一旦通过 configure 方法自定义 AuthenticationManager实现 就回将工厂中自动配置AuthenticationManager 进行覆盖
//2. 一旦通过 configure 方法自定义 AuthenticationManager实现 需要在实现中指定认证数据源对象 UserDetailService 实例
//3. 一旦通过 configure 方法自定义 AuthenticationManager实现 这种方式创建AuthenticationManager对象工厂内部本地一个 AuthenticationManager 对象 不允许在其他自定义组件中进行注入
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userDetailsServiceImpl);
}
/**
* BCryptPasswordEncoder相关知识:
* 用户表的密码通常使用MD5等不可逆算法加密后存储,为防止彩虹表破解更会先使用一个特定的字符串(如域名)加密,然后再使用一个随机的salt(盐值)加密。
* 特定字符串是程序代码中固定的,salt是每个密码单独随机,一般给用户表加一个字段单独存储,比较麻烦。
* BCrypt算法将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题。
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//HttpSecurity配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors(withDefaults())
// 禁用 CSRF
.csrf().disable()
.authorizeRequests()
// 指定的接口直接放行
// swagger
.antMatchers(SecurityConstants.SWAGGER_WHITELIST).permitAll()
.antMatchers(SecurityConstants.H2_CONSOLE).permitAll()
.antMatchers(HttpMethod.POST, SecurityConstants.SYSTEM_WHITELIST).permitAll()
// 其他的接口都需要认证后才能请求
.anyRequest().authenticated()
.and()
//添加自定义Filter
.addFilter(new JwtAuthorizationFilter(authenticationManager(), stringRedisTemplate))
// 不需要session(不创建会话)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 授权异常处理
.exceptionHandling()
// json提示用户没有登录不需要用户跳转到登录页面去
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
// 权限拦截器,提示用户没有当前权限
.accessDeniedHandler(new JwtAccessDeniedHandler());
// 防止H2 web 页面的Frame 被拦截
http.headers().frameOptions().disable();
}
/**
* Cors配置优化
**/
@Bean
CorsConfigurationSource corsConfigurationSource() {
org.springframework.web.cors.CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(singletonList("*"));
// configuration.setAllowedOriginPatterns(singletonList("*"));
configuration.setAllowedHeaders(singletonList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "DELETE", "PUT", "OPTIONS"));
configuration.setExposedHeaders(singletonList(SecurityConstants.TOKEN_HEADER));
configuration.setAllowCredentials(false);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}