application.xml
首先引进redis的相关配置 token是要存在这里的
#Mon Nov 04 11:33:55 CST 2019
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.shutdown-timeout=100
spring.redis.lettuce.pool.max-wait=10000
spring.redis.host=127.0.0.1
spring.redis.timeout=10000
授权服务器
AuthorizationServer1 .java
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer1 extends
AuthorizationServerConfigurerAdapter {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory()
.withClient("password1")
.authorizedGrantTypes("password","refresh_token")
.accessTokenValiditySeconds(1800)
//这个其实就是权限更粗一点的 例如ROLE_CMS_NEWS,ROLE_APP_INFO,ROLE_KVCONFIG,ROLE_NEWS_COMMENT这几个微服务的权限 由自已灵活使用 前面几个是微服务名
.authorities("ROLE_ADMIN")
//这个也就是权限更高一点的 针对于资源服务器 需要和资源服务器一一对应 都设置成一样就行
.resourceIds("rid")
//这个也是针对于资源服务器的
.scopes("read");
}
//用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
//这个会自动把认证信息 存入存入到redis 只要你配了数据源 在掉接口的时候/oauth/token
//比如access_token key名就为 access:e18e1f70-d89d-4c70-b6bf-1f3a7036b310 在用户认证的时候可以直接拿key 看存不存在
endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
//这个啊 就是让他支持密码模式 否则会报 不支持 密码模式
.authenticationManager(authenticationManager)
//这个可以用来验证用户名 如果你自已定义的的userDetailsService 没有也得加上 默认的
//否则会报错Handling error: IllegalStateException, UserDetailsService is required.
.userDetailsService(userDetailsService);
}
//用来配置令牌端点(Token Endpoint)的安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security)
throws Exception {
//允许check_token 这个接口是用来解token 的 看以看到token 存放具体什么信息
security.checkTokenAccess("permitAll()")
//主要是让/oauth/token支持client_id以及client_secret作登录认证 否则会报Unauthorized
.allowFormAuthenticationForClients()//允许表单认证
;
}
}
资源服务器 ResouceServerConfig.class 可以配置多个资源服务器
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.AuthenticationEntryPoint;
@Configuration
public class ResouceServerConfig {
@Bean
public WebResponseExceptionTranslator webResponseExceptionTranslator() {
return new DefaultWebResponseExceptionTranslator() {
@Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
OAuth2Exception body = responseEntity.getBody();
// 认证失败(过期)
if (e instanceof InsufficientAuthenticationException) {
body.addAdditionalInformation("code", "610");
body.addAdditionalInformation("msg", body.getOAuth2ErrorCode());
}
HttpHeaders headers = new HttpHeaders();
headers.setAll(responseEntity.getHeaders().toSingleValueMap());
// do something with header or response
return new ResponseEntity<>(body, headers, responseEntity.getStatusCode());
}
};
}
/**
* 测试资源服务
*/
@Configuration
@EnableResourceServer
public class ResouceTestServerConfig extends
ResourceServerConfigurerAdapter {
// @Autowired
// private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources)
throws Exception {
resources.resourceId("rid")
//只能是基于令牌的认证方式 默认就是true
.stateless(true);
// 定义异常转换类生效
AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
((OAuth2AuthenticationEntryPoint) authenticationEntryPoint).setExceptionTranslator(webResponseExceptionTranslator());
resources.authenticationEntryPoint(authenticationEntryPoint);
}
@Override
public void configure(HttpSecurity http) throws Exception {
//不管配置多少个资源服务器 会自动合并成一个 没有冲突的合并 有冲突的前面覆盖后面 比如这个session
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//下面这句其实OAUTH是根据FilterChain 里面有十几个过滤器
//一般有三个SecurityConfig一个 还有默认的也就是oauth/tocken oauth/check_token等等 也是一个 这个默认是默认的所有没要必要
//在配置什么permitAll 还有就是这个资源服务器里 不过你配置多少个资源服务器 最终会汇总到一个
//优先级oauthToken>资源服务器>SecurityConfig 下面这个就是匹配一下匹配到就执行这个chain匹配不到就交给下一级调用链
//但是假如不写这个 那他也不会到下一级 也就是说SecurityConfig的HttpSecurity
//基本没有调用的机会 因为默认的RequestMatch是NotOAuthRequestMatcher 最后用ReuquestMatcher调用链
.requestMatchers()
.antMatchers("/uua/**")
.and()
.authorizeRequests()
.antMatchers("/uua/fegin/**").permitAll()
.antMatchers("/uua/fegin1/**")
//客户端也有更细粒度啊的权限控制
.access("#oauth2.hasScope('read') and #oauth2.clientHasRole('ROLE_ADMIN')")
//下面这个表示掐他请求都需要认证
.anyRequest().authenticated();
// and
// hasRole('ROLE_USER')
}
}
}
security.config
package com.example.demo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//这个必须要 因为比如说下面这个 他会选择不把登录填的密码加密与实际密码比对 如果是其他方式会加密
//密码比对是有这个框架自动做的 (至少我现在写的这种方式是)
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
//尤其注意这个 AuthenticationManagerBuilder 因为父类重构了好几个configure方法认准这个就是做认证的
//就是下面这个.anyRequest().authenticated() 会来这里认证
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("admin").password("123456").roles("admin")
.and().withUser("sang").password("123456").roles("ADMIN");
}
//基本没啥调用机会 因为资源服务器就给拦住了
@Override
protected void configure(HttpSecurity http) throws Exception {
}
//不定义没有password grant_type
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
return super.userDetailsService();
}
}
下面说一下调用流程 (我随便说的哦 不一定准确)
看一下 /oauth/token经历了啥
总所周知 他这个需要 经历 三个步骤 用户认证 发放令牌 令牌在跑到资源服务器 去校验一下令牌 然后就去访问了
首先 用户认证吧 其实这个框架 主要就是围绕一个过滤器 来进行 就一个一个的进去过滤器进去执行
有三个过滤器
匹配到那个就去执行哪个 第一个过滤器链是oauth/token 那几个默认的端点 第二个就是我们的资源服务器里 第三个就是
那个SecurityConfig
很明显匹配到了第一个过滤器链
在下面这个类里get一下客户端的相关信息 username是前台传的clientId
然后把客户端的信息存入到UserDetails username是前台传clientId 然后后面把前台传过来的客户端密码和后台存的客户端密码比较一下比较的类和方法是
DaoAuthenticationProvider.additionalAuthenticationChecks
DaoAuthenticationProvider.class
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
//看见没 这个this.passwordEncoder会把前台传来密码加密一下 这下就知道 为什么必须配置这个Bean了吧
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
AbstractSecurityInterceptor.class
//这个方法很关键 因为在具体取资源的时候这个就是关键最后的一把决定要不要放行
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
boolean debug = this.logger.isDebugEnabled();
if (!this.getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + this.getSecureObjectClass());
} else {
//取出我当前这个访问链接需不需要认证 需要认证的话 需要什么权限
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
if (attributes != null && !attributes.isEmpty()) {
if (debug) {
this.logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
this.credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes);
}
//我当前的链接已经有什么权限
Authentication authenticated = this.authenticateIfRequired();
try {
//把已经有的和需要有的比较一下看是否放行
this.accessDecisionManager.decide(authenticated, object, attributes);
} catch (AccessDeniedException var7) {
this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var7));
throw var7;
}
if (debug) {
this.logger.debug("Authorization successful");
}
if (this.publishAuthorizationSuccess) {
this.publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
if (runAs == null) {
if (debug) {
this.logger.debug("RunAsManager did not change Authentication object");
}
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
} else {
if (debug) {
this.logger.debug("Switching to RunAs Authentication: " + runAs);
}
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
} else if (this.rejectPublicInvocations) {
throw new IllegalArgumentException("Secure object invocation " + object + " was denied as public invocations are not allowed via this interceptor. This indicates a configuration error because the rejectPublicInvocations property is set to 'true'");
} else {
if (debug) {
this.logger.debug("Public object - authentication not attempted");
}
this.publishEvent(new PublicInvocationEvent(object));
return null;
}
}
}
这个端口有就开始真正的检验客户端权限 以及scope 上面方法的这个
OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
就是认证用户方法Token
ResourceOwnerPasswordTokenGranter.class
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
String username = (String)parameters.get("username");
String password = (String)parameters.get("password");
parameters.remove("password");
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
((AbstractAuthenticationToken)userAuth).setDetails(parameters);
Authentication userAuth;
try {
//去认证了
userAuth = this.authenticationManager.authenticate(userAuth);
} catch (AccountStatusException var8) {
throw new InvalidGrantException(var8.getMessage());
} catch (BadCredentialsException var9) {
throw new InvalidGrantException(var9.getMessage());
}
if (userAuth != null && userAuth.isAuthenticated()) {
OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
} else {
throw new InvalidGrantException("Could not authenticate user: " + username);
}
这样的话就把用户信息生成token 前面有声明RedisTokenStore这个Bean 他会自动把token信息存入到redis里
再去资源服务器取得时候key+前台传的token能取到就是认证过 没取到就是没有
{
"access_token": "7dde09dc-12d7-4dc9-b3fc-6521adf51507",
"token_type": "bearer",
"refresh_token": "36869a92-278b-4975-b7ea-f7d26ecb84c5",
"expires_in": 601,
"scope": "read"
}
于是到这里 用户认证 发放令牌到这里就结束了
然后去资源服务器取值
例如http://10.0.75.1:8083/uua/fegin1/test?access_token=7dde09dc-12d7-4dc9-b3fc-6521adf51507
访问一个受限的资源 其实和上面差不多 一直到
AbstractSecurityInterceptor.
beforeInvocation的最后一个方法
具体去认证
OAuth2AuthenticationProcessingFilter.class
Authentication authResult = this.authenticationManager.authenticate(authentication);