spring security 整合 oauth2
前言
之前一篇博客学过security的核心,这次整合一下oauth2,它也是市场上比较流行的接口验证的一种方式了
引入pom
文中提及的整合oauth2的方式是建立在boot 的基础上的.在引入的boot 和security的start之后,我们还需要引入oauth2,注意,它不是start,另外我们计划将token存储在redis中,所以我们还需要引入redis的start
<!-- 将token存储在redis中 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
学习代码
security基本配置,主要是用户权限,以及设置oauth的认证路径为所以角色都可访问\
@Configuration
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers().anyRequest()
.and()
.authorizeRequests()
.antMatchers("/oauth/*").permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
System.out.println("开始认证角色..............");
//这里设置的角色 系统会自动加上role_前缀 既ROLE_ADMIN
//inMemoryAuthentication 从内存中获取
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("user_1").password(new BCryptPasswordEncoder().encode("12345678")).roles("client");
//inMemoryAuthentication 从内存中获取
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("user_2").password(new BCryptPasswordEncoder().encode("12345678")).roles("USER");
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
//oauth2的配置中需要这个bean
return super.authenticationManagerBean();
}
}
oauth2配置;如你所见oauth是建立在sercurity的基础上的,所以资源服务器和认证服务器的configure与security基本的configure如出一辙;所谓资源服务器配置就是值用户请求资源时,哪些资源能被访问,能被怎样的权限所访问的配置,认证服务器的配置主要是指采用哪种方式进行认证,认证的客户端的账号密码的配置.关于认证方式有四种,这里只提了两种;
-
授权码模式(authorization code)
-
简化模式(implicit)
-
密码模式(resource owner password credentials)
-
客户端模式(client credentials)
@Configuration
public class Oauth2ServerConfig {
private static final String DEMO_RESOURCE_ID="order";
/**
* 资源服务器配置
*/
@Configuration
@EnableResourceServer
protected static class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public ResourceServerConfig() {
super();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.requestMatchers().anyRequest()
.and()
.anonymous()
.and()
.authorizeRequests()
//设置请求资源需要认证
.antMatchers("/getOrderInfo/**").authenticated();
}
}
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private AuthenticationManager authenticationManager;
@Bean
protected PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
public AuthorizationServerConfig() {
super();
}
@Override
/**
* 配置authorizationServer安全认证的相关信息,创建clientCredentialsTokenEndPointFilter核心过滤器
*/
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
@Override
/**
* 配置oauth2的客户端相关信息
*/
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("client1")
.resourceIds(DEMO_RESOURCE_ID)
//客户端模式
.authorizedGrantTypes("client_credentials","refresh_token")
.scopes("select")
.authorities("ROLE_CLIENT")
//系统默认只接受加密的密码
.secret(passwordEncoder.encode("123456"))
.and().withClient("cilent2")
.resourceIds(DEMO_RESOURCE_ID)
//password模式
.authorizedGrantTypes("password","refresh_token")
.scopes("select")
.authorities("client")
//a62d747e-91fb-42c5-8857-b47243179ecd
.secret(passwordEncoder.encode("123456"));
}
@Override
/**
* 配置AuthorizationServerEndpointsConfigurer众多相关类,包括配置身份认证器,配置认证方式,TokenStore,TokenGranter,OAuth2RequestFactory
*/
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
.authenticationManager(authenticationManager);
}
}
}
两个资源请求路径的测试controller
@RestController
public class TestEndPoint {
@GetMapping("/getProductInfo/{id}")
public String getProductInfo(@PathVariable String id){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication.getDetails());
System.out.println(authentication.getPrincipal());
return "product id:" +id;
}
@GetMapping("/getOrderInfo/{id}")
public String getOrderInfo(@PathVariable String id){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication.getDetails());
System.out.println(authentication.getPrincipal());
return "order id:" +id;
}
}
核心说明
1.请求进入ClientCredentialsTokenEndpointFilter中的doFilter方法,跑到attemptAuthentication()开始进行认证
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
***********
Authentication authResult;
//开始认证
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
******************
}
2.获取用户传入的认证服务器的client_id和client_secret 组装成UsernamePasswordAuthenticationToken,传入AuthenticationManager(默认是providerManager)进行认证.
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
**************************************
String clientId = request.getParameter("client_id");
String clientSecret = request.getParameter("client_secret");
**************************************
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
clientSecret);
return this.getAuthenticationManager().authenticate(authRequest);
}
3.providerManager循环调用注册的provider进行认证,一般用的是daoAutheticationProvider
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
***************************
try {
//调用provider进行认证
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
******************************
throw lastException;
}
4.daoAuthenticationProvider会调用userService获取数据库或内存中的用户信息,这里我们是存在内存中的.
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
//从缓存中获取用户信息
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//准备调用userdetailService去获取用户信息
user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
}
}
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//调用userService去获取用户信息(这里是指认证服务器的信息)
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
************************
}
5.provider拿到用户输入的服务器的账号密码的token以及保存在数据库或者内存中的账号密码信息之后便在additionalAuthenticationChecks中开始认证(主要是调用加密器进行密码匹配),如果没有抛出异常则算成功
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"));
}
}
6.认证成功继续往后走,provider将会将userDetail里的权限塞入token,返回给providermanager
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
try {
//加载userDetail
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
***********************
try {
//账号认证前检查,是否可用,是否过期,是否锁住
preAuthenticationChecks.check(user);
//账号认证
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
*************
//账号认证后检查,是否过期
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//创建塞入权限的token并返回给providerManager
return createSuccessAuthentication(principalToReturn, authentication, user);
}
7.providerManager拿到token之后将会移除密码,来保证系统安全,然后返回ClientCredentialsTokenEndpointFilter
try {
//认证
result = provider.authenticate(authentication);
if (result != null) {
//移除token的密码
copyDetails(authentication, result);
break;
}
}
8.认证成功后,进入successfulAuthetication方法
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
*******************
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
****************
successfulAuthentication(request, response, chain, authResult);
}
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
***************************
successHandler.onAuthenticationSuccess(request, response, authResult);
}
9.认证成功之后,请求开始走如同我们一般请求的流程,进入dispatchServlet,dispatcher处理我们请求之后,通过/oauth/token映射到TokenEndPoint类
//这个mapping很关键
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
//获取服务器账号
String clientId = getClientId(principal);
//通过账号获取服务器信息,账号,密码,权限,域等
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
//获取tokenRequest对象,主要包含了服务器i信息的账号密码 权限,认证方式,以及请求参数
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
*****************************
//进入tokenGranter准备办法accessToken
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
10.请求进入tokenGranter
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
}
String clientId = tokenRequest.getClientId();
//获取认证服务器信息
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
validateGrantType(grantType, client);
if (logger.isDebugEnabled()) {
logger.debug("Getting access token for: " + clientId);
}
return getAccessToken(client, tokenRequest);
}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
11.实际颁发者是defaulTokenService,这个类负责刷新,添加,移除accessToken等一切相关accessToken的相关操作
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null) {
if (existingAccessToken.isExpired()) {
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
// The token store could remove the refresh token when the
// access token is removed, but we want to
// be sure...
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
else {
// Re-store the access token in case the authentication has changed
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}
// But the refresh token itself might need to be re-issued if it has
// expired.
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
//创建accessToken
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
****************
return accessToken;
}
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
//我们收到的accessToken就是这个类序列化后的样子
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
12 返回dispatchServlet中的doFilter,认证流程结束
后记
有空再分析一下资源请求的源码,写博客还是很费时间的..................
推荐阅读
-
Spring Security认证提供程序示例详解
-
3行代码快速实现Spring Boot Oauth2服务功能
-
使用spring整合Quartz实现—定时器功能
-
SSH整合中 hibernate托管给Spring得到SessionFactory
-
详解spring cloud整合Swagger2构建RESTful服务的APIs
-
Spring Boot整合mybatis(一)实例代码
-
spring Boot与Mybatis整合优化详解
-
Spring Boot整合RabbitMQ开发实战详解
-
使用Spring Security控制会话的方法
-
spring整合atomikos实现分布式事务的方法示例