Spring Security OAuth2快速开始
授权服务器

Authorize Endpoint :授权端点,进行授权
Token Endpoint :令牌端点,经过授权拿到对应的Token
Introspection Endpoint :校验端点,校验Token的合法性
Revocation Endpoint :撤销端点,撤销授权
整体架构

流程: 1. 用户访问,此时没有Token。Oauth2RestTemplate会报错,这个报错信息会被Oauth2ClientContextFilter捕获并重定向到认证服务器。 2. 认证服务器通过Authorization Endpoint进行授权,并通过AuthorizationServerTokenServices生成授权码并返回给客户端。 3. 客户端拿到授权码去认证服务器通过Token Endpoint调用AuthorizationServerTokenServices生成Token并返回给客户端 4. 客户端拿到Token去资源服务器访问资源,一般会通过Oauth2AuthenticationManager调用ResourceServerTokenServices进行校验。校验通过可以获取资源。
授权码模式
引入依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.4.RELEASE</version> </dependency>
或者 引入spring cloud oauth2依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!-- spring cloud -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
配置spring security @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin().permitAll() .and().authorizeRequests() .antMatchers("/oauth/**").permitAll() .anyRequest().authenticated() .and().logout().permitAll() .and().csrf().disable(); } } @Service public class UserService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { String password = passwordEncoder.encode("123456"); return new User("fox",password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } } @RestController @RequestMapping("/user") public class UserController { @RequestMapping("/getCurrentUser") public Object getCurrentUser(Authentication authentication) { return authentication.getPrincipal(); } }
配置授权服务器 @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private PasswordEncoder passwordEncoder; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() //配置client_id .withClient("client") //配置client-secret .secret(passwordEncoder.encode("123123")) //配置访问token的有效期 .accessTokenValiditySeconds(3600) //配置刷新token的有效期 .refreshTokenValiditySeconds(864000) //配置redirect_uri,用于授权成功后跳转 .redirectUris("http://www.baidu.com") //配置申请的权限范围 .scopes("all") //配置grant_type,表示授权类型 .authorizedGrantTypes("authorization_code"); } }
配置资源服务器 @Configuration @EnableResourceServer public class ResourceServiceConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and().requestMatchers().antMatchers("/user/**"); } }
测试 获取授权码 http://localhost:8080/oauth/authorize?response_type=code&client_id=client 或者 http://localhost: 8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com &scope=all 登录之后进入



grant_type :授权类型,填写authorization_code,表示授权码模式
code :授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
client_id :客户端标识
redirect_uri :申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
scope :授权范围。 认证失败服务端返回 401 Unauthorized
访问资源
根据token去资源服务器获取资源



简化模式
authorizedGrantType添加implicit

测试 http://localhost:8080/oauth/authorize?client_id=client&response_type=token&scope=all&redirect_ uri=http://www.baidu.com 登录之后进入授权页面,确定授权后浏览器会重定向到指定路径,并以Hash的形式存放在重定向uri的 fargment中:

密码模式
修改WebSecurityConfig,增加AuthenticationManager @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin().permitAll() .and().authorizeRequests() .antMatchers("/oauth/**").permitAll() .anyRequest().authenticated() .and().logout().permitAll() .and().csrf().disable(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
修改AuthorizationServerConfig配置 @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig2 extends AuthorizationServerConfigurerAdapter { @Autowired private PasswordEncoder passwordEncoder; @Autowired private AuthenticationManager authenticationManagerBean; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需 要配置 . allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持 GET,POST请求 } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { //允许表单认证 security.allowFormAuthenticationForClients(); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { /** * 授权码模式 *http://localhost:8080/oauth/authorize? response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all *http://localhost:8080/oauth/authorize? response_type=code&client_id=client * * password模式 * http://localhost:8080/oauth/token? username=fox&password=123456&grant_type=password&client_id=client&client_secret= 123123&scope=all * * 客户端模式 * http://localhost:8080/oauth/token? grant_type=client_credentials&scope=all&client_id=client&client_secret=123123 * / clients.inMemory() //配置client_id .withClient("client") //配置client-secret .secret(passwordEncoder.encode("123123")) //配置访问token的有效期 .accessTokenValiditySeconds(3600) //配置刷新token的有效期 .refreshTokenValiditySeconds(864000) //配置redirect_uri,用于授权成功后跳转 .redirectUris("http://www.baidu.com") //配置申请的权限范围 .scopes("all") /** * 配置grant_type,表示授权类型 * authorization_code: 授权码 * password: 密码 * client_credentials: 客户端 * / .authorizedGrantTypes("authorization_code","password","client_credentials"); } }
获取令牌
通过浏览器测试,需要配置支持get请求和表单验证 http://localhost:8080/oauth/token?username=fox&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all

通过Postman测试

访问资源

客户端模式

更新令牌
使用oauth2时,如果令牌失效了,可以使用刷新令牌通过refresh_token的授权模式再次获取access_token。只需修改认证服务器的配置,添加refresh_token的授权模式即可。
修改授权服务器配置,增加refresh_token配置
@Autowired private UserService userService; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配 置 // .tokenStore(tokenStore) //指定token存储到redis . reuseRefreshTokens(false) //refresh_token是否重复使用 . userDetailsService(userService) //刷新令牌授权包含对用户信息的检查 . allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支 持GET,POST请求 } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { /** * 授权码模式 *http://localhost:8080/oauth/authorize? response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all *http://localhost:8080/oauth/authorize? response_type=code&client_id=client * * password模式 * http://localhost:8080/oauth/token? username=fox&password=123456&grant_type=password&client_id=client&client_secret= 123123&scope=all * * 客户端模式 * http://localhost:8080/oauth/token? grant_type=client_credentials&scope=all&client_id=client&client_secret=123123 * / clients.inMemory() //配置client_id .withClient("client") //配置client-secret .secret(passwordEncoder.encode("123123")) //配置访问token的有效期 .accessTokenValiditySeconds(3600) //配置刷新token的有效期 .refreshTokenValiditySeconds(864000) //配置redirect_uri,用于授权成功后跳转 .redirectUris("http://www.baidu.com") //配置申请的权限范围 .scopes("all") /** * 配置grant_type,表示授权类型 * authorization_code: 授权码 * password: 密码 * client_credentials: 客户端 * refresh_token: 更新令牌 * / .authorizedGrantTypes("authorization_code","password","client_credentials","refr esh_token"); }
通过密码模式测试

http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=1231&refresh_token=dc03bdc2-ca3b-4690-9265-d31a21896d02

基于redis存储Token
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
修改application.yaml spring: redis: host: 127.0.0.1 database: 0
编写redis配置类 @Configuration public class RedisConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore tokenStore(){ return new RedisTokenStore(redisConnectionFactory); } }
在授权服务器配置中指定令牌的存储策略为Redis @Autowired private TokenStore tokenStore; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置 .tokenStore(tokenStore) //指定token存储到redis .reuseRefreshTokens(false) //refresh_token是否重复使用 .userDetailsService(userService) //刷新令牌授权包含对用户信息的检查 .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支 持GET,POST请求 }
原理分析
授权服务器 以客户端模式为例 http://localhost:8080/oauth/token?grant_type=client_credentials&scope=select&client_id=client_1 &client_secret=123456 ClientCredentialsTokenEndpointFilter //客户端身份认证核心过滤器 DaoAuthenticationProvider // 提供身份认证 TokenEndpoint //Token处理端点 TokenGranter // Token授与者,用来颁发Token
@EnableAuthorizationServer
@Configuration @EnableAuthorizationServer protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {} public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer { // 配置AuthorizationServer安全认证的相关信息,创建 ClientCredentialsTokenEndpointFilter核心过滤器 @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception{ } // 配置OAuth2的客户端相关信息 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { } // 配置AuthorizationServerEndpointsConfigurer众多相关类,包括配置身份认证器,配置认 证方式,TokenStore,TokenGranter,OAuth2RequestFactory @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { } }
ClientCredentialsTokenEndpointFilter 客户端身份认证核心过滤器 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { ... String clientId = request.getParameter("client_id"); String clientSecret = request.getParameter("client_secret"); ... clientId = clientId.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId, clientSecret); return this.getAuthenticationManager().authenticate(authRequest); }
AuthenticationManager

在ClientDetailsUserDetailsService中将client客户端的信息(client_id,client_secret)封装成用户的信息(username,password)
TokenEndpoint
Token处理端点 @FrameworkEndpoint public class TokenEndpoint extends AbstractEndpoint { @RequestMapping(value = "/oauth/token", method=RequestMethod.POST) public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { ... String clientId = getClientId(principal); // 加载客户端信息 ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId); ... //结合请求信息,创建TokenRequest TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient); ... //将TokenRequest传递给TokenGranter颁发token OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest); ... return getResponse(token); } private TokenGranter tokenGranter; }
TokenGranter

TokenGranter的设计思路是使用CompositeTokenGranter管理一个List列表,每一种grantType对应一个具体的真正授权者,CompositeTokenGranter 内部就是在循环调用五种TokenGranter实现类的 grant方法,而granter内部则是通过grantType来区分是否是各自的授权类型。
五种类型分别是: ResourceOwnerPasswordTokenGranter ==> password密码模式 AuthorizationCodeTokenGranter ==> authorization_code授权码模式 ClientCredentialsTokenGranter ==> client_credentials客户端模式 ImplicitTokenGranter ==> implicit简化模式 RefreshTokenGranter ==>refresh_token 刷新token专用
OAuth2AccessToken
@org.codehaus.jackson.map.annotate.JsonSerialize(using = OAuth2AccessTokenJackson1Serializer.class) @org.codehaus.jackson.map.annotate.JsonDeserialize(using = OAuth2AccessTokenJackson1Deserializer.class) @com.fasterxml.jackson.databind.annotation.JsonSerialize(using = OAuth2AccessTokenJackson2Serializer.class) @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = OAuth2AccessTokenJackson2Deserializer.class) public interface OAuth2AccessToken { public static String BEARER_TYPE = "Bearer"; public static String OAUTH2_TYPE = "OAuth2"; public static String ACCESS_TOKEN = "access_token"; public static String TOKEN_TYPE = "token_type"; public static String EXPIRES_IN = "expires_in"; public static String REFRESH_TOKEN = "refresh_token"; public static String SCOPE = "scope"; ... }
AuthorizationServerTokenServices public interface AuthorizationServerTokenServices { //创建token OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException; //刷新token OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest) throws AuthenticationException; //获取token OAuth2AccessToken getAccessToken(OAuth2Authentication authentication); }
创建token时,会调用tokenStore对产生的token和相关信息存储到对应的实现类中,可以是redis,数据库,内存,jwt。
TokenStore

资源服务器
@EnableResourceServer
public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer { @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { } @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated(); } }
ResourceServerSecurityConfigurer
public void configure(HttpSecurity http) throws Exception { AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http); //创建OAuth2核心过滤器 resourcesServerFilter = new OAuth2AuthenticationProcessingFilter(); resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint); //设置OAuth2的身份认证处理器,没有交给spring管理(避免影响非普通的认证流程) resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager); if (eventPublisher != null) { resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher); } if (tokenExtractor != null) { //设置TokenExtractor默认的实现BearerTokenExtractor resourcesServerFilter.setTokenExtractor(tokenExtractor); } resourcesServerFilter = postProcess(resourcesServerFilter); resourcesServerFilter.setStateless(stateless); // @formatter:off http .authorizeRequests().expressionHandler(expressionHandler) .and() .addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class) .exceptionHandling() .accessDeniedHandler(accessDeniedHandler)//相关的异常处理器,可以重写相关 实现,达到自定义异常的目的 .authenticationEntryPoint(authenticationEntryPoint); // @formatter:on }
OAuth2AuthenticationProcessingFilter OAuth2保护资源的预先认证过滤器 通过携带access_token可以访问受限资源 http://localhost:8080/order/1?access_token=a4e4ccb0-9a51-479a-a86c-376410fd0c00

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { final HttpServletRequest request = (HttpServletRequest) req; final HttpServletResponse response = (HttpServletResponse) res; try { //从请求中取出身份信息,即access_token,封装到 PreAuthenticatedAuthenticationToken Authentication authentication = tokenExtractor.extract(request); if (authentication == null) { ... } else { request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal()); if (authentication instanceof AbstractAuthenticationToken) { AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication; needsDetails.setDetails(authenticationDetailsSource.buildDetails(request)); } //认证身份 Authentication authResult = authenticationManager.authenticate(authentication); ... eventPublisher.publishAuthenticationSuccess(authResult); //将身份信息绑定到SecurityContextHolder中 SecurityContextHolder.getContext().setAuthentication(authResult); } } catch (OAuth2Exception failed) { ... return; } chain.doFilter(request, response); }
TokenExtractor
提取出请求中包含的token Header中携带 作为request请求参数 access_token public interface TokenExtractor { /** * 在不进行身份验证的情况下从传入请求提取令牌值 * / Authentication extract(HttpServletRequest request); } http://localhost:8080/order/1 Header: Authentication:Bearer a4e4ccb0-9a51-479a-a86c-376410fd0c00 http://localhost:8080/order/1?access_token=a4e4ccb0-9a51-479a-a86c- 376410fd0c00 http://localhost:8080/order/1 form param: access token=a4e4ccb0-9a51-479a-a86c-376410fd0c00
OAuth2AuthenticationManager

public Authentication authenticate(Authentication authentication) throws AuthenticationException { ... String token = (String) authentication.getPrincipal(); //借助ResourceServerTokenServices根据token加载客户端身份信息 OAuth2Authentication auth = tokenServices.loadAuthentication(token); ... checkClientDetails(auth); if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) { OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails(); ... } auth.setDetails(authentication.getDetails()); auth.setAuthenticated(true); return auth; }
ResourceServerTokenServices public interface ResourceServerTokenServices { //根据accessToken加载客户端信息 OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException; //根据accessToken获取完整的访问令牌详细信息。 OAuth2AccessToken readAccessToken(String accessToken); }
JWT
什么是JWT JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC 算法或使用RSA的公钥/私钥对来签名,防止被篡改。 官网: https://jwt.io/ 标准: https://tools.ietf.org/html/rfc7519
JWT令牌的优点: 1. jwt基于json,非常方便解析。 2. 可以在令牌中自定义丰富的内容,易扩展。 3. 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。 4. 资源服务使用JWT可不依赖认证服务即可完成授权。
缺点: JWT令牌较长,占存储空间比较大。
JWT组成
一个JWT实际上就是一个字符串,它由三部分组成,头部(header)、载荷(payload)与签名(signature)。

头部(header)
头部用于描述关于该JWT的最基本的信息:类型(即JWT)以及签名所用的算法(如HMACSHA256或RSA)等。 这也可以被表示成一个JSON对象: 然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分: { "alg": "HS256", "typ":"JWT" } eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷(payload)
第二部分是载荷,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含 三个部分: 标准中注册的声明(建议但不强制使用) iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过期时间,这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 公共的声明 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信 息.但不建议添加敏感信息,因为该部分在客户端可解密. 私有的声明 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64是对称解密的,意味着该部分信息可以归类为明文信息。 定义一个payload: 然后将其进行base64加密,得到Jwt的第二部分: { "sub": "1234567890", "name":"John Doe", "iat": 1516239022 } eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
签名(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成: header (base64后的) payload (base64后的) secret(盐,一定要保密) 这个部分需要base64加密后的header和base64加密后的payload使用 . 连接组成的字符串,然后通过 header中声明的加密方式进行加盐 secret 组合加密,然后就构成了jwt的第三部分: 将这三部分用 . 连接成一个完整的字符串,构成了最终的jwt:
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和 jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, 'fox'); //
khA7TNYc7_0iELcDyTc7gHBZ_xfIcgbfpzUNWwQtzMEeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.khA7TNYc7_0iELcDyTc7gHBZ_xfIcgbfpzUNWwQtzME
如何应用
一般是在请求头里加入 Authorization ,并加上 Bearer 标注: 服务端会验证token,如果验证通过就会返回相应的资源。整个流程就是这样的: fetch('api/user/1', { headers: { ' Authorization': 'Bearer ' + token } })

JWT快速开始
引入依赖 <!--JWT依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
创建token 创建测试类,生成token @Test public void test() { //创建一个JwtBuilder对象 JwtBuilder jwtBuilder = Jwts.builder() //声明的标识{"jti":"666"} . setId("666") //主体,用户{"sub":"Fox"} . setSubject("Fox") //创建日期{"ita":"xxxxxx"} . setIssuedAt(new Date()) //签名手段,参数1:算法,参数2:盐 . signWith(SignatureAlgorithm.HS256, "123123"); //获取token String token = jwtBuilder.compact(); System.out.println(token); //三部分的base64解密 System.out.println("========="); String[] split = token.split("\\."); System.out.println(Base64Codec.BASE64.decodeToString(split[0])); System.out.println(Base64Codec.BASE64.decodeToString(split[1])); //无法解密 System.out.println(Base64Codec.BASE64.decodeToString(split[2])); }
运行结果

token的验证解析
在web应用中由服务端创建了token然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token应该解析出token中的信息(例如用
户id),根据这些信息查询数据库返回相应的结果
public void testParseToken(){ //token String token=
"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiJGb3giLCJpYXQiOjE2MDgyNzI1NDh9" + ".Hz7tk6pJaest_jxFrJ4BWiMg3HQxjwY9cGmJ4GQwfuU"; //解析token获取载荷中的声明对象 Claims claims = Jwts.parser() .setSigningKey("123123") .parseClaimsJws(token) .getBody(); System.out.println("id:"+claims.getId()); System.out.println("subject:"+claims.getSubject()); System.out.println("issuedAt:"+claims.getIssuedAt()); }

token过期校验
有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。原因:从服务器发出的token,服务器自己并不做记录,就存在一个弊端:服务端无法主动控制某个token
的立刻失效。

自定义claims
我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以自定义claims。
@Test public void test() { //创建一个JwtBuilder对象 JwtBuilder jwtBuilder = Jwts.builder() //声明的标识{"jti":"666"} .setId("666") //主体,用户{"sub":"Fox"} .setSubject("Fox") //创建日期{"ita":"xxxxxx"} .setIssuedAt(new Date()) //设置过期时间 1分钟 .setExpiration(new Date(System.currentTimeMillis()+60*1000)) //直接传入map // .addClaims(map) .claim("roles","admin") .claim("logo","xxx.jpg") //签名手段,参数1:算法,参数2:盐 .signWith(SignatureAlgorithm.HS256, "123123"); //获取token String token = jwtBuilder.compact(); System.out.println(token); //三部分的base64解密 System.out.println("========="); String[] split = token.split("\\."); System.out.println(Base64Codec.BASE64.decodeToString(split[0])); System.out.println(Base64Codec.BASE64.decodeToString(split[1])); //无法解密 System.out.println(Base64Codec.BASE64.decodeToString(split[2])); } @Test public void testParseToken(){ //token String token= "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiJGb3giLCJpYXQiOjE2MDgyNzYzMTUsImV4cCI6MTYwODI3NjM3NSwicm9sZXMiOiJhZG1pbiIsImxvZ28iOiJ4eHguanBnIn0.Geg2tmkmJ9iWC WdvZNE3jRSfRaXaR4P3kiPDG3Lb0z4"; //解析token获取载荷中的声明对象 Claims claims = Jwts.parser() .setSigningKey("123123") .parseClaimsJws(token) .getBody(); System.out.println("id:"+claims.getId()); System.out.println("subject:"+claims.getSubject()); System.out.println("issuedAt:"+claims.getIssuedAt()); DateFormat sf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println("签发时间:"+sf.format(claims.getIssuedAt())); System.out.println("过期时间:"+sf.format(claims.getExpiration())); System.out.println("当前时间:"+sf.format(new Date())); System.out.println("roles:"+claims.get("roles")); System.out.println("logo:"+claims.get("logo")); }

Spring Security Oauth2整合JWT
整合JWT 在之前的spring security Oauth2的代码基础上修改 引入依赖 <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.9.RELEASE</version> </dependency>
添加配置文件JwtTokenStoreConfig.java @Configuration public class JwtTokenStoreConfig { @Bean public TokenStore jwtTokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); //配置JWT使用的秘钥 accessTokenConverter.setSigningKey("123123"); return accessTokenConverter; } }

发现获取到的令牌已经变成了JWT令牌,将access_token拿到https://jwt.io/ 网站上去解析下可以获得其中内容。

扩展JWT中的存储内容
有时候我们需要扩展JWT中存储的内容,这里我们在JWT中扩展一个 key为enhance,value为enhanceinfo 的数据。 继承TokenEnhancer实现一个JWT内容增强器
public class JwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Map<String, Object> info = new HashMap<>(); info.put("enhance", "enhance info"); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); return accessToken; } }
创建一个JwtTokenEnhancer实例在授权服务器配置中配置JWT的内容增强器
@Bean public JwtTokenEnhancer jwtTokenEnhancer() { return new JwtTokenEnhancer(); } @Autowired private JwtTokenEnhancer jwtTokenEnhancer; @Override 运行项目后使用密码模式来获取令牌,之后对令牌进行解析,发现已经包含扩展的内容。 public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //配置JWT的内容增强器 TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> delegates = new ArrayList<>(); delegates.add(jwtTokenEnhancer); delegates.add(jwtAccessTokenConverter); enhancerChain.setTokenEnhancers(delegates); endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置 .tokenStore(tokenStore) //配置存储令牌策略 .accessTokenConverter(jwtAccessTokenConverter) .tokenEnhancer(enhancerChain) //配置tokenEnhancer .reuseRefreshTokens(false) //refresh_token是否重复使用 .userDetailsService(userService) //刷新令牌授权包含对用户信息的检查 .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支 持GET,POST请求 }

解析JWT
添加依赖 <!--JWT依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
修改UserController类,使用jjwt工具类来解析Authorization头中存储的JWT内容 @GetMapping("/getCurrentUser") public Object getCurrentUser(Authentication authentication, HttpServletRequest request) { String header = request.getHeader("Authorization"); String token = null; if(header!=null){ token = header.substring(header.indexOf("bearer") + 7); }else { 将令牌放入Authorization头中,访问如下地址获取信息: http://localhost:8080/user/getCurrentUser token = request.getParameter("access_token"); } return Jwts.parser() .setSigningKey("123123".getBytes(StandardCharsets.UTF_8)) .parseClaimsJws(token) .getBody(); }

刷新令牌 http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=[refresh_token值]

Spring Secuirty Oauth2实现SSO
单系统登录机制 http无状态协议 web应用采用browser/server架构,http作为通信协议。http是无状态协议,浏览器的每一次请求,服 务器会独立处理,不与之前或之后的请求产生关联,这个过程用下图说明,三次请求/响应对之间没有任 何联系。

但这也同时意味着,任何用户都能通过浏览器访问服务器资源,如果想保护服务器的某些资源,必须限制浏览器请求;要限制浏览器请求,必须鉴别浏览器请求,响应合法请求,忽略非法请求;要鉴别浏览
器请求,必须清楚浏览器请求状态。既然http协议无状态,那就让服务器和浏览器共同维护一个状态吧!这就是会话机制
会话机制
浏览器第一次请求服务器,服务器创建一个会话,并将会话的id作为响应的一部分发送给浏览器,浏览器存储会话id,并在后续第二次和第三次请求中带上会话id,服务器取得请求中的会话id就知道是不是同
一个用户了,这个过程用下图说明,后续请求与第一次请求产生了关联。

服务器在内存中保存会话对象,浏览器怎么保存会话id? 请求参数 将会话id作为每一个请求的参数,服务器接收请求自然能解析参数获得会话id,并借此判断是否来 自同一会话,很明显,这种方式不靠谱。 cookie 浏览器自己来维护这个会话id,每次发送http请求时浏览器自动发送会话id,cookie机制正好用来 做这件事。cookie是浏览器用来存储少量数据的一种机制,数据以”key/value“形式存储,浏览器发送 http请求时自动附带cookie信息。 tomcat会话机制当然也实现了cookie,访问tomcat服务器时,浏览器中可以看到一个名为 “JSESSIONID”的cookie,这就是tomcat会话机制维护的会话id。 使用了cookie的请求响应过程如下图
登录状态
有了会话机制,登录状态就好理解了,我们假设浏览器第一次请求服务器需要输入用户名与密码验证身份,服务器拿到用户名密码去数据库比对,正确的话说明当前持有这个会话的用户是合法用户,应该将 这个会话标记为“已授权”或者“已登录”等等之类的状态,既然是会话的状态,自然要保存在会话对象中。
tomcat在会话对象中设置登录状态如下 用户再次访问时,tomcat在会话对象中查看登录状态 实现了登录状态的浏览器请求服务器模型如下图描述
HttpSession session = request.getSession(); session.setAttribute("isLogin", true); HttpSession session = request.getSession(); session.getAttribute("isLogin");

每次请求受保护资源时都会检查会话对象中的登录状态,只有 isLogin=true 的会话才能访问,登录机制因此而实现。
多系统的复杂性
web系统早已从久远的单系统发展成为如今由多系统组成的应用群,面对如此众多的系统,用户难道要一个一个登录、然后一个一个注销吗?就像下图描述的这样:

web系统由单系统发展成多系统组成的应用群,复杂性应该由系统内部承担,而不是用户。无论web系统内部多么复杂,对用户而言,都是一个统一的整体,也就是说,用户访问web系统的整个应用群与访
问单个系统一样,登录/注销只要一次就够了

单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器与服务器之间维护会话状态。但cookie是有限制的,这个限制就是cookie的域(通常对应网站的域名),浏览器发送http请求时会自动
携带与该域匹配的cookie,而不是所有cookie

既然这样,为什么不将web应用群中所有子系统的域名统一在一个顶级域名下,例如“*.baidu.com”,然后将它们的cookie域设置为“baidu.com”,这种做法理论上是可以的,甚至早期很多多系统登录就采用
这种同域名共享cookie的方式。
然而,可行并不代表好,共享cookie的方式存在众多局限。首先,应用群域名得统一;其次,应用群各系统使用的技术(至少是web服务器)要相同,不然cookie的key值(tomcat为JSESSIONID)不同,无
法维持会话,共享cookie的方式是无法实现跨语言技术平台登录的,比如java、php、.net系统之间;第三,cookie本身不安全。
因此,我们需要一种全新的登录方式来实现多系统应用群的登录,这就是单点登录
单点登录
什么是单点登录?单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分。
登录
相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证
用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式
相同。
这个过程,也就是单点登录的原理,用下图说明:

下面对上图简要描述 1. 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为 参数 2. sso认证中心发现用户未登录,将用户引导至登录页面 3. 用户输入用户名密码提交登录申请 4. sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令 牌 5. sso认证中心带着令牌跳转会最初的请求地址(系统1) 6. 系统1拿到令牌,去sso认证中心校验令牌是否有效 7. sso认证中心校验令牌,返回有效,注册系统1 8. 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源 9. 用户访问系统2的受保护资源 10. 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数 11. sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌 12. 系统2拿到令牌,去sso认证中心校验令牌是否有效 13. sso认证中心校验令牌,返回有效,注册系统2 14. 系统2使用该令牌创建与用户的局部会话,返回受保护资源 用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局 会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将 不再通过sso认证中心,全局会话与局部会话有如下约束关系 1. 局部会话存在,全局会话一定存在 2. 全局会话存在,局部会话不一定存在 3. 全局会话销毁,局部会话必须销毁
注销
单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁,用下面的图来说明

sso认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作下面对上图简要说明
1. 用户向系统1发起注销请求 2. 系统1根据用户与系统1建立的会话id拿到令牌,向sso认证中心发起注销请求 3. sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址 4. sso认证中心向所有注册系统发起注销请求 5. 各注册系统接收sso认证中心的注销请求,销毁局部会话 6. sso认证中心引导用户至登录页面
架构
单点登录涉及sso认证中心与众子系统,子系统与sso认证中心需要通信以交换令牌、校验令牌及发起注销请求,因而子系统必须集成sso的客户端,sso认证中心则是sso服务端,整个单点登录过程实质是sso
客户端与服务端通信的过程,用下图描述

sso认证中心与sso客户端通信方式有多种,httpClient,web service、rpc、restful api都可以。
Spring Secuirty Oauth2实现
创建客户端:oauth2-sso-client-demo 引入依赖 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <!--JWT依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
修改application.properties server.port=8081 #防止Cookie冲突,冲突会导致登录验证不通过 server.servlet.session.cookie.name=OAUTH2-CLIENT-SESSIONID01 #授权服务器地址 oauth2-server-url: http://localhost:8080 #与授权服务器对应的配置 security.oauth2.client.client-id=client security.oauth2.client.client-secret=123123 security.oauth2.client.user-authorization-uri=${oauth2- serverurl}/oauth/authorize security.oauth2.client.access-token-uri=${oauth2-server-url}/oauth/token security.oauth2.resource.jwt.key-uri=${oauth2-server-url}/oauth/token_key
在启动类上添加@EnableOAuth2Sso注解来启用单点登录功能 @SpringBootApplication @EnableOAuth2Sso public class Oauth2SsoClientDemoApplication { public static void main(String[] args) { SpringApplication.run(Oauth2SsoClientDemoApplication.class, args); } }
添加接口用于获取当前登录用户信息 @RestController @RequestMapping("/user") public class UserController { @RequestMapping("/getCurrentUser") public Object getCurrentUser(Authentication authentication) { return authentication; } }
授权服务器:oauth2-jwt-demo 修改授权服务器中的AuthorizationServerConfig类,将绑定的跳转路径为http://localhost:8081/login,并添加获取秘钥时的身份认证

@Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { //允许表单认证 security.allowFormAuthenticationForClients() // 获取密钥需要身份认证,使用单点登录时必须配置 . tokenKeyAccess("isAuthenticated()"); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() //配置client_id .withClient("client") //配置client-secret .secret(passwordEncoder.encode("123123")) //配置访问token的有效期 .accessTokenValiditySeconds(3600) //配置刷新token的有效期 .refreshTokenValiditySeconds(864000) //配置redirect_uri,用于授权成功后跳转 .redirectUris("http://localhost:8081/login") //自动授权配置 .autoApprove(true) 测试 启动授权服务和客户端服务; 访问客户端需要授权的接口http://localhost:8081/user/getCurrent User 会跳转到授权服务的登录界面; //配置申请的权限范围 .scopes("all") /** * 配置grant_type,表示授权类型 * authorization_code: 授权码 * password: 密码 * client_credentials: 客户端 * refresh_token: 更新令牌 * / .authorizedGrantTypes("authorization_code","password","refresh_token"); }
测试 启动授权服务和客户端服务; 访问客户端需要授权的接口http://localhost:8081/user/getCurrentUser 会跳转到授权服务的登录界面;
授权后会跳转到原来需要权限的接口地址,展示登录用户信息

模拟两个客户端8081,8082 修改application.yaml配置 修改授权服务器配置,配置多个跳转路径 8081登录成功之后,8082无需再次登录就可以访问http://localhost:8082/user/getCurrentUser server.port=8082 #防止Cookie冲突,冲突会导致登录验证不通过 server.servlet.session.cookie.name=OAUTH2-CLIENT-SESSIONID${server.port} //配置redirect_uri,用于授权成功后跳转 .redirectUris("http://localhost:8081/login", "http://localhost:8082/login")

浙公网安备 33010602011771号