文章目录

  • 一、JWT
    • 1.1 什么是JWT
    • 1.2 JWT组成
      • 头部(header)
      • 载荷(payload)
      • 签名(signature)
      • 如何应用
    • 1.3 JJWT
      • 快速开始
      • 创建token
      • token的验证解析
      • token过期校验
      • 自定义claims
    • 1.4 Spring Security Oauth2整合JWT
      • 整合JWT
      • 扩展JWT中的存储内容
      • 解析JWT
      • 刷新令牌
  • 二、Spring Secuirty Oauth2实现SSO
    • 创建客户端服务:`mall-oauth2-sso-client`
    • 授权服务器配置修改
    • 模拟单点登录
  • 三、Spring Cloud中如何实现SSO

一、JWT

1.1 什么是JWT

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。

官网: https://jwt.io/
标准: https://tools.ietf.org/html/rfc7519

JWT令牌的优点

  • jwt基于json,非常方便解析。
  • 可以在令牌中自定义丰富的内容,易扩展。
  • 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
  • 资源服务使用JWT可不依赖授权服务即可完成授权。

缺点:
JWT令牌较长,占存储空间比较大。

1.2 JWT组成

一个JWT实际上就是一个字符串,它由三部分组成,头部(header)、载荷(payload)与签名(signature)。

头部(header)

头部用于描述关于该JWT的最基本的信息:类型(即JWT)以及签名所用的算法(如HMACSHA256或RSA)等。

这也可以被表示成一个JSON对象:

{"alg": "HS256","typ": "JWT"
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

载荷(payload)

第二部分是载荷,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:

1、标准中注册的声明(建议但不强制使用)

  • iss:jwt签发者
  • sub:jwt所面向的用户
  • aud:接收jwt的一方
  • exp:jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf:定义在什么时间之前,该jwt都是不可用的.
  • iat:jwt的签发时间
  • jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

2、公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

3、私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:

{"sub": "1234567890","name": "John Doe","iat": 1516239022
}

然后将其进行base64加密,得到Jwt的第二部分:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

签名(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret(盐,一定要保密)

这个部分需要base64加密后的header和base64加密后的payload使用。连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分:

var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);var signature = HMACSHA256(encodedString, 'fox'); // khA7TNYc7_0iELcDyTc7gHBZ_xfIcgbfpzUNWwQtzME

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.khA7TNYc7_0iELcDyTc7gHBZ_xfIcgbfpzUNWwQtzME

注意: secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

如何应用

一般是在请求头里加入Authorization,并加上Bearer标注:

fetch('api/user/1', {headers: {'Authorization': 'Bearer ' + token}
})

服务端会验证token,如果验证通过就会返回相应的资源。整个流程就是这样的:

1.3 JJWT

JJWT是一个提供端到端的JWT创建和验证的Java库,永远免费和开源(Apache License,版本2.0)。JJW很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

快速开始

引入依赖:

 <!--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":"Jihu"}.setSubject("Jihu")//创建日期{"ita":"xxxxxx"}.setIssuedAt(new Date())//签名手段,参数1:算法,参数2:盐.signWith(SignatureAlgorithm.HS256, "123123");//获取tokenString 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),根据这些信息查询数据库返回相应的结果。

@Testpublic void testParseToken() {//tokenString token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiJKaWh1IiwiaWF0IjoxNjQwNjUzNTEyfQ.gxfHSE1NFsar8kG9dGJrN0EPmMk5U6cVCjgCh8Hlzoc";//解析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是永久生效的,所以我们可以为token添加一个过期时间。原因:从服务器发出的token,服务器自己并不做记录,就存在一个弊端:服务端无法主动控制某个token的立刻失效。

@Test
public void test() {//创建一个JwtBuilder对象JwtBuilder jwtBuilder = Jwts.builder()//声明的标识{"jti":"666"}.setId("666")//主体,用户{"sub":"Jihu"}.setSubject("Jihu")//创建日期{"ita":"xxxxxx"}.setIssuedAt(new Date())// 设置过期时间为1分钟.setExpiration(new Date(System.currentTimeMillis() + 60 * 1000))//签名手段,参数1:算法,参数2:盐.signWith(SignatureAlgorithm.HS256, "123123");//获取tokenString 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]));
}

当未过期时可以正常读取,当过期时会引发io.jsonwebtoken.ExpiredJwtException异常。

自定义claims

我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以自定义claims。

@Test
public void test() {//创建一个JwtBuilder对象JwtBuilder jwtBuilder = Jwts.builder()//声明的标识{"jti":"666"}.setId("666")//主体,用户{"sub":"Jihu"}.setSubject("Jihu")//创建日期{"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");//获取tokenString 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(){//tokenString token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiJGb3giLCJpYXQiOjE2MDgyNzYzMTUsImV4cCI6MTYwODI3NjM3NSwicm9sZXMiOiJhZG1pbiIsImxvZ28iOiJ4eHguanBnIn0.Geg2tmkmJ9iWCWdvZNE3jRSfRaXaR4P3kiPDG3Lb0z4";//解析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"));
}

1.4 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 {@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();// 配置JWT使用的秘钥accessTokenConverter.setSigningKey("123123");return accessTokenConverter;}@Beanpublic TokenStore jwtTokenStore() {return new JwtTokenStore(jwtAccessTokenConverter());}}

在授权服务器配置AuthorizationServerConfig中指定令牌的存储策略为JWT:

@Configuration
@EnableAuthorizationServer // 这个注解必须加上
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Lazy@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate AuthenticationManager authenticationManagerBean;@Autowiredprivate UserService userService;//    @Autowired
//    private TokenStore tokenStore;@Autowired@Qualifier("jwtTokenStore")private TokenStore tokenStore;@Autowiredprivate JwtAccessTokenConverter jwtAccessTokenConverter;// 配置支持GET,POST请求@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST) //支持GET,POST请求.reuseRefreshTokens(false)  //refresh_token是否重复使用.userDetailsService(userService) //刷新令牌授权包含对用户信息的检查.tokenStore(tokenStore)  // 配置存储令牌策略.accessTokenConverter(jwtAccessTokenConverter); // token转化器,我们转为了JWT}// 配置允许表单认证@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security.allowFormAuthenticationForClients();}@Overridepublic 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=token&client_id=client&redirect_uri=http://www.baidu.com&scope=allclients.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:授权码模式*  implicit:简化模式*  password:密码模式*  client_credentials: 客户端模式*  refresh_token: 更新令牌*/.authorizedGrantTypes("authorization_code", "implicit", "password", "client_credentials", "refresh_token");}
}

用密码模式测试:

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

扩展JWT中的存储内容

有时候我们需要扩展JWT中存储的内容,这里我们在JWT中扩展一个 key为enhance,value为enhance info 的数据。

继承TokenEnhancer实现一个JWT内容增强器:

public class JwtTokenEnhancer implements TokenEnhancer {@Overridepublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {// 自定义暴露信息
//        MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal();
//
//        final Map<String, Object> additionalInfo = new HashMap<>();
//
//        final Map<String, Object> retMap = new HashMap<>();
//
//        //todo 这里暴露memberId到Jwt的令牌中,后期可以根据自己的业务需要 进行添加字段
//        additionalInfo.put("memberId",memberDetails.getUmsMember().getId());
//        additionalInfo.put("nickName",memberDetails.getUmsMember().getNickname());
//        additionalInfo.put("integration",memberDetails.getUmsMember().getIntegration());
//
//        retMap.put("additionalInfo",additionalInfo);Map<String, Object> info = new HashMap<>();info.put("enhance", "enhance info");((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);return accessToken;}
}

在AuthorizationServerConfig类中创建一个JwtTokenEnhancer实例:

// JwtTokenStoreConfig类中添加配置
@Bean
public JwtTokenEnhancer jwtTokenEnhancer() {return new JwtTokenEnhancer();
}

在授权服务器配置中配置JWT的内容增强器:

@Configuration
@EnableAuthorizationServer // 这个注解必须加上
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Lazy@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate AuthenticationManager authenticationManagerBean;@Autowiredprivate UserService userService;//    @Autowired
//    private TokenStore tokenStore;@Autowired@Qualifier("jwtTokenStore")private TokenStore tokenStore;@Autowiredprivate JwtAccessTokenConverter jwtAccessTokenConverter;@Autowiredprivate JwtTokenEnhancer jwtTokenEnhancer;// 配置支持GET,POST请求@Overridepublic 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) //使用密码模式需要配置.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST) //支持GET,POST请求.reuseRefreshTokens(false)  //refresh_token是否重复使用.userDetailsService(userService) //刷新令牌授权包含对用户信息的检查.tokenStore(tokenStore)  // 配置存储令牌策略.accessTokenConverter(jwtAccessTokenConverter) // token转化器,我们转为了JWT.tokenEnhancer(enhancerChain); //配置tokenEnhancer}// 配置允许表单认证@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security.allowFormAuthenticationForClients();}@Overridepublic 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=token&client_id=client&redirect_uri=http://www.baidu.com&scope=allclients.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:授权码模式*  implicit:简化模式*  password:密码模式*  client_credentials: 客户端模式*  refresh_token: 更新令牌*/.authorizedGrantTypes("authorization_code", "implicit", "password", "client_credentials", "refresh_token");}
}

运行项目后使用密码模式来获取令牌,之后对令牌进行解析,发现已经包含扩展的内容。

解析JWT

添加依赖:

<!--JWT依赖-->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency>

修改UserController类,增加一个接口来使用jjwt工具类来解析Authorization头中存储的JWT内容。

@GetMapping("/parseJWT")
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 {token = request.getParameter("access_token");}return Jwts.parser().setSigningKey("123123".getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
}

注意:如果之前配置了RedisConfig#RedisTokenStore,要将其删除掉!否则可能出现错误说不能将access_token转化为JWT的错误!

将令牌放入Authorization头中,访问如下地址获取信息:
http://localhost:8080/user/parseJWT

刷新令牌

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

二、Spring Secuirty Oauth2实现SSO

创建客户端服务:mall-oauth2-sso-client


引入依赖:

<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>

配置application.yml文件:

spring:application:name: mall-oauth2-sso-clientserver:port: 8081#防止Cookie冲突,冲突会导致登录验证不通过servlet:session:cookie:name: OAUTH2-CLIENT-SESSIONID01#授权服务器地址(我们之前的授权服务地址为:http://localhost:8080)
oauth2-server-url: http://localhost:8080#与授权服务器对应的配置
security:oauth2:client:client-id: clientclient-secret: 123123user-authorization-uri: ${oauth2-serverurl}/oauth/authorizeaccess-token-uri: ${oauth2-server-url}/oauth/tokenresource:jwt:key-uri: ${oauth2-server-url}/oauth/token_key

在启动类上添加@EnableOAuth2Sso注解来启用单点登录功能 。

@EnableOAuth2Sso单点登录的原理简单来说就是:标注有@EnableOAuth2Sso的OAuth2 Client应用在通过某种OAuth2授权流程获取访问令牌后(一般是授权码流程),通过访问令牌访问userDetails用户明细这个受保护资源服务,获取用户信息后,将用户信息转换为Spring Security上下文中的认证后凭证Authentication,从而完成标注有@EnableOAuth2Sso的OAuth2 Client应用自身的登录认证的过程。整个过程是基于OAuth2的SSO单点登录

@SpringBootApplication
@EnableOAuth2Sso
public class MallOauth2SsoClientApplication {public static void main(String[] args) {SpringApplication.run(MallOauth2SsoClientApplication.class, args);}
}

添加接口用于获取当前登录用户信息:

@RestController
@RequestMapping("/user")
public class UserController {@RequestMapping("/getCurrentUser")public Object getCurrentUser(Authentication authentication) {return authentication;}
}

授权服务器配置修改

修改授权服务器中的AuthorizationServerConfig类:
1、将绑定的跳转路径为http://localhost:8081/login,并添加获取秘钥时的身份认证。
2、添加tokenKeyAccess("isAuthenticated()");

@Configuration
@EnableAuthorizationServer // 这个注解必须加上
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Lazy@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate AuthenticationManager authenticationManagerBean;@Autowiredprivate UserService userService;@Autowired@Qualifier("jwtTokenStore")private TokenStore tokenStore;@Autowiredprivate JwtAccessTokenConverter jwtAccessTokenConverter;@Autowiredprivate JwtTokenEnhancer jwtTokenEnhancer;// 配置支持GET,POST请求@Overridepublic 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) //使用密码模式需要配置.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST) //支持GET,POST请求.reuseRefreshTokens(false)  //refresh_token是否重复使用.userDetailsService(userService) //刷新令牌授权包含对用户信息的检查.tokenStore(tokenStore)  // 配置存储令牌策略.accessTokenConverter(jwtAccessTokenConverter) // token转化器,我们转为了JWT.tokenEnhancer(enhancerChain); //配置tokenEnhancer}// 配置允许表单认证@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security.allowFormAuthenticationForClients()// 获取密钥需要身份认证,使用单点登录时必须配置.tokenKeyAccess("isAuthenticated()");}@Overridepublic 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=token&client_id=client&redirect_uri=http://www.baidu.com&scope=allclients.inMemory()// 配置client_id.withClient("client")// 配置client_secret.secret(passwordEncoder.encode("123123"))//配置访问token的有效期.accessTokenValiditySeconds(3600)//配置刷新token的有效期.refreshTokenValiditySeconds(864000)//配置redirect_uri,用于授权成功后跳转.redirectUris("http://localhost:8081/login")//配置申请的权限范围.scopes("all")//自动授权配置.autoApprove(true)/*** 配置grant_type,表示授权类型**  authorization_code:授权码模式*  implicit:简化模式*  password:密码模式*  client_credentials: 客户端模式*  refresh_token: 更新令牌*/.authorizedGrantTypes("authorization_code", "implicit", "password", "client_credentials", "refresh_token");}
}

测试:

分别启动授权服务客户端服务

访问客户端需要授权的接口(访问资源,此时没有完成认证,所以访问后会自动跳转到授权服务的登录界面):http://localhost:8081/user/getCurrentUser

会跳转到授权服务的登录界面(完成认证后才能正常访问资源)。

因为我们配置了自动approve功能,所以输入用户名和密码后会直接调回原来的路径并成功获取到数据:

{"authorities": [{"authority": "admin"}],"details": {"remoteAddress": "0:0:0:0:0:0:0:1","sessionId": "5F4C388D7E9B6707BC7120539E7558D3","tokenValue": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJqaWh1Iiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTY0MDY5MTY4MCwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiMTc4OTk0M2QtOGYyOC00MjdkLTllZWItYzZkZjIyZDNkN2Q2IiwiY2xpZW50X2lkIjoiY2xpZW50IiwiZW5oYW5jZSI6ImVuaGFuY2UgaW5mbyJ9.Q_tRiAtZv33QSePdOA1nZYZjq8O7Iex8uiypwsx_W5I","tokenType": "bearer","decodedDetails": null},"authenticated": true,"userAuthentication": {"authorities": [{"authority": "admin"}],"details": null,"authenticated": true,"principal": "jihu","credentials": "N/A","name": "jihu"},"credentials": "","principal": "jihu","clientOnly": false,"oauth2Request": {"clientId": "client","scope": ["all"],"requestParameters": {"client_id": "client"},"resourceIds": [],"authorities": [],"approved": true,"refresh": false,"redirectUri": null,"responseTypes": [],"extensions": {},"grantType": null,"refreshTokenRequest": null},"name": "jihu"
}

授权后会跳转到原来需要权限的接口地址,展示登录用户信息。

模拟单点登录

模拟两个客户端8081,8091。

修改原来客户端服务 8081的端口为8091,准备再启动一台客户端服务。

server:port: 8091#防止Cookie冲突,冲突会导致登录验证不通过servlet:session:cookie:name: OAUTH2-CLIENT-SESSIONID01

然后修改授权服务器配置,配置多个跳转路径:

//配置redirect_uri,用于授权成功后跳转
.redirectUris("http://localhost:8081/login","http://localhost:8091/login")

然后重新启动授权服务,并将客户端8081和8091都启动起来:

测试:
先访问:http://localhost:8081/user/getCurrentUser

会跳转到登录授权服务界面,需要完成授权。

8081登录成功并成功请求到数据之后,我们再来访问:
http://localhost:8091/user/getCurrentUser

发现8091不需要认证登录就可以直接完成访问了。这样就实现了单点登录的功能。

三、Spring Cloud中如何实现SSO

网关整合 OAuth2.0 有两种思路:

  • 一种是授权服务器生成令牌, 所有请求统一在网关层验证,判断权限等操作;
  • 另一种是由各资源服务处理,网关只做请求转发。

比较常用的是第一种,把API网关作为OAuth2.0的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当前登录用户信息给微服务,这样下游微服务就不需要关心令牌格式解析以及OAuth2.0相关机制了。

网关在认证授权体系里主要负责两件事:

(1)作为OAuth2.0的资源服务器角色,实现接入方权限拦截。
(2)令牌解析并转发当前登录用户信息(明文token)给微服务

微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事:

(1)用户授权拦截(看当前用户是否有权访问该资源)
(2)将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)


核心代码,网关自定义全局过滤器进行身份认证:

@Component
@Order(0)
public class AuthenticationFilter implements GlobalFilter, InitializingBean {@Autowiredprivate RestTemplate restTemplate;private static Set<String> shouldSkipUrl = new LinkedHashSet<>();@Overridepublic void afterPropertiesSet() throws Exception {// 不拦截认证的请求shouldSkipUrl.add("/oauth/token");shouldSkipUrl.add("/oauth/check_token");shouldSkipUrl.add("/user/getCurrentUser");}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String requestPath = exchange.getRequest().getURI().getPath();//不需要认证的urlif(shouldSkip(requestPath)) {return chain.filter(exchange);}//获取请求头String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");//请求头为空if(StringUtils.isEmpty(authHeader)) {throw new RuntimeException("请求头为空");}TokenInfo tokenInfo=null;try {//获取token信息tokenInfo = getTokenInfo(authHeader);}catch (Exception e) {throw new RuntimeException("校验令牌异常");}exchange.getAttributes().put("tokenInfo",tokenInfo);return chain.filter(exchange);}private boolean shouldSkip(String reqPath) {for(String skipPath:shouldSkipUrl) {if(reqPath.contains(skipPath)) {return true;}}return false;}private TokenInfo getTokenInfo(String authHeader) {// 获取token的值String token = StringUtils.substringAfter(authHeader, "bearer ");HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);headers.setBasicAuth(MDA.clientId, MDA.clientSecret);MultiValueMap<String, String> params = new LinkedMultiValueMap<>();params.add("token", token);HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);ResponseEntity<TokenInfo> response = restTemplate.exchange(MDA.checkTokenUrl, HttpMethod.POST, entity, TokenInfo.class);return response.getBody();}
}

JWT实战 Spring Security Oauth2整合JWT 整合SSO单点登录相关推荐

  1. 基于 Spring Security OAuth2和 JWT 构建保护微服务系统

    我们希望自己的微服务能够在用户登录之后才可以访问,而单独给每个微服务单独做用户权限模块就显得很弱了,从复用角度来说是需要重构的,从功能角度来说,也是欠缺的.尤其是前后端完全分离之后,我们的用户信息不一 ...

  2. 《深入理解 Spring Cloud 与微服务构建》第十八章 使用 Spring Security OAuth2 和 JWT 保护微服务系统

    <深入理解 Spring Cloud 与微服务构建>第十八章 使用 Spring Security OAuth2 和 JWT 保护微服务系统 文章目录 <深入理解 Spring Cl ...

  3. 使用Spring Security Oauth2 和 JWT保护微服务--Uaa授权服务器的编写

    学习自深入理解微服务 采用Spring Security OAuth2 和 JWT的方式,Uaa服务只需要验证一次,返回JWT.返回的JWT包含了用户的所有信息,包括权限信息 从三个方面讲解: JWT ...

  4. Spring Security Oauth2 授权码模式下 自定义登录、授权页面

    主要说明:基于若依springcloud微服务框架的2.1版本 嫌弃缩进不舒服的,直接访问我的博客站点: http://binarydance.top//aticle_view.html?aticle ...

  5. Spring Cloud OAuth2 实现用户认证及单点登录

    OAuth 2 有四种授权模式,分别是授权码模式(authorization code).简化模式(implicit).密码模式(resource owner password credentials ...

  6. 爆破专栏丨Spring Security系列教程之实现CAS单点登录上篇-概述

    作者:千锋一一哥 前言 从本章节开始,一一哥 会给各位讲解一个很常见也很重要的知识点,就是单点登录!现在的大型分布式项目,基本都会考虑实现单点登录,而且现在网上也有很多单点登录的实现方案.开源项目,但 ...

  7. java培训爆破专栏之Spring Security系列教程之实现CAS单点登录上篇-概述

    作者:千锋一一哥 前言 从本章节开始,一一哥 会给各位讲解一个很常见也很重要的知识点,就是单点登录!现在的大型分布式项目,基本都会考虑实现单点登录,而且现在网上也有很多单点登录的实现方案.开源项目,但 ...

  8. 使用Spring Security Oauth2 和 JWT保护微服务--资源服务器的编写

    编写hcnet-website的资源服务 依赖管理pom文件 hcnet-website工程的pom文件继承主maven的pom文件.在hcnet-website工程的pom文件中添加web功能的起步 ...

  9. Spring Security Oauth2 解析jwt

最新文章

  1. Kali Linux安装VMware Tools
  2. SGI重获新生 有望下月摆脱破产保护
  3. Linux centos7安装Git及 Jenkins配置Git
  4. 常见Linux面试题总结
  5. 关于 SAP Spartacus UI 框架选型问题
  6. 【算法图解|4】JavaScript 如何求数组的最大值和最小值
  7. html修改details范围,HTML details 标签
  8. 手把手带你玩转Tensorflow 物体检测 API (3)——训练模型
  9. Cobertura代码覆盖率测试
  10. windows环境下32位汇编语言程序设计 90盘_程序设计作业题汇总
  11. matlab2c使用c++实现matlab函数系列教程-binostat函数
  12. 计算机自带的桌面远程,如何远程控制电脑桌面
  13. 华为机型深色模式下问题
  14. .net 开源混淆器 ConfuserEx
  15. Jmeter的基本使用
  16. 网页局部打印以及如何设置打印机纸张大小
  17. pad看linux源码,在 iPad和 iPhone的浏览器上查看网页源代码
  18. 12. 整数转罗马数字
  19. Word代做,线上兼职,月入过万
  20. 座舱人机交互「暗潮汹涌」,语音「下」,多模态「上」

热门文章

  1. 他们怎样读书和选书(汇总篇)
  2. ESP32-C3使用LEDC外设实现呼吸灯效果
  3. 大学计算机课程制作生日卡片,DIY手工制作生日贺卡
  4. 游戏二次元场景插画教程,常见的6种构图方法分享
  5. 成都睿铂|超详细三维模型单体化技术流程与标准
  6. Tableau数据分析笔记-Chapter13雷达图和凹凸图
  7. 强化学习系列5:有模型的策略迭代方法
  8. HTML_实体、meta标签
  9. 【转载】2019 年,国内博士后的招聘要求和待遇是怎样的?
  10. windows以兼容模式运行程序