- 一、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
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令牌较长,占存储空间比较大。
一个JWT实际上就是一个字符串,它由三部分组成,头部(header)、载荷(payload)与签名(signature)。
头部用于描述关于该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,如果验证通过就会返回相应的资源。整个流程就是这样的:
JJWT是一个提供端到端的JWT创建和验证的Java库,永远免费和开源(Apache License,版本2.0)。JJW很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
快速开始引入依赖:
创建tokenio.jsonwebtoken jjwt0.9.1
创建测试类,生成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"); //获取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])); }
运行结果:
在web应用中由服务端创建了token然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。
@Test public void testParseToken() { //token String 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的立刻失效。
@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"); //获取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])); }
当未过期时可以正常读取,当过期时会引发io.jsonwebtoken.ExpiredJwtException异常。
我们刚才的例子只是存储了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"); //获取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.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的代码基础上修改。
引入依赖:
org.springframework.security spring-security-jwt1.0.9.RELEASE
添加配置文件JwtTokenStoreConfig.java :
@Configuration public class JwtTokenStoreConfig { @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); // 配置JWT使用的秘钥 accessTokenConverter.setSigningKey("123123"); return accessTokenConverter; } @Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } }
在授权服务器配置AuthorizationServerConfig中指定令牌的存储策略为JWT:
@Configuration @EnableAuthorizationServer // 这个注解必须加上 public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Lazy @Autowired private PasswordEncoder passwordEncoder; @Autowired private AuthenticationManager authenticationManagerBean; @Autowired private UserService userService; // @Autowired // private TokenStore tokenStore; @Autowired @Qualifier("jwtTokenStore") private TokenStore tokenStore; @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; // 配置支持GET,POST请求 @Override public 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 } // 配置允许表单认证 @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=token&client_id=client&redirect_uri=http://www.baidu.com&scope=all 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") .authorizedGrantTypes("authorization_code", "implicit", "password", "client_credentials", "refresh_token"); } }
用密码模式测试:
发现获取到的令牌已经变成了JWT令牌,将access_token拿到https://jwt.io/ 网站上去解析下可以获得其中内容。
有时候我们需要扩展JWT中存储的内容,这里我们在JWT中扩展一个 key为enhance,value为enhance info 的数据。
继承TokenEnhancer实现一个JWT内容增强器:
public class JwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { // 自定义暴露信息 // MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); // // final MapadditionalInfo = new HashMap<>(); // // final Map 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 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 @Autowired private PasswordEncoder passwordEncoder; @Autowired private AuthenticationManager authenticationManagerBean; @Autowired private UserService userService; // @Autowired // private TokenStore tokenStore; @Autowired @Qualifier("jwtTokenStore") private TokenStore tokenStore; @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired private JwtTokenEnhancer jwtTokenEnhancer; // 配置支持GET,POST请求 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //配置JWT的内容增强器 TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); Listdelegates = 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 } // 配置允许表单认证 @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=token&client_id=client&redirect_uri=http://www.baidu.com&scope=all 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") .authorizedGrantTypes("authorization_code", "implicit", "password", "client_credentials", "refresh_token"); } }
运行项目后使用密码模式来获取令牌,之后对令牌进行解析,发现已经包含扩展的内容。
添加依赖:
io.jsonwebtoken jjwt0.9.1
修改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值]
引入依赖:
org.springframework.boot spring-boot-starter-weborg.springframework.cloud spring-cloud-starter-oauth2io.jsonwebtoken jjwt0.9.1 org.springframework.boot spring-boot-starter-testtest
配置application.yml文件:
spring: application: name: mall-oauth2-sso-client server: port: 8081 #防止cookie冲突,冲突会导致登录验证不通过 servlet: session: cookie: name: OAUTH2-CLIENT-SESSIONID01 #授权服务器地址(我们之前的授权服务地址为:http://localhost:8080) oauth2-server-url: http://localhost:8080 #与授权服务器对应的配置 security: oauth2: client: client-id: client client-secret: 123123 user-authorization-uri: ${oauth2-serverurl}/oauth/authorize access-token-uri: ${oauth2-server-url}/oauth/token resource: 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 @Autowired private PasswordEncoder passwordEncoder; @Autowired private AuthenticationManager authenticationManagerBean; @Autowired private UserService userService; @Autowired @Qualifier("jwtTokenStore") private TokenStore tokenStore; @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired private JwtTokenEnhancer jwtTokenEnhancer; // 配置支持GET,POST请求 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //配置JWT的内容增强器 TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); Listdelegates = 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 } // 配置允许表单认证 @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients() // 获取密钥需要身份认证,使用单点登录时必须配置 .tokenKeyAccess("isAuthenticated()"); } @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=token&client_id=client&redirect_uri=http://www.baidu.com&scope=all 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") //配置申请的权限范围 .scopes("all") //自动授权配置 .autoApprove(true) .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不需要认证登录就可以直接完成访问了。这样就实现了单点登录的功能。
网关整合 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 { @Autowired private RestTemplate restTemplate; private static SetshouldSkipUrl = new linkedHashSet<>(); @Override public void afterPropertiesSet() throws Exception { // 不拦截认证的请求 shouldSkipUrl.add("/oauth/token"); shouldSkipUrl.add("/oauth/check_token"); shouldSkipUrl.add("/user/getCurrentUser"); } @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { String requestPath = exchange.getRequest().getURI().getPath(); //不需要认证的url if(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 params = new linkedMultiValueMap<>(); params.add("token", token); HttpEntity > entity = new HttpEntity<>(params, headers); ResponseEntity response = restTemplate.exchange(MDA.checkTokenUrl, HttpMethod.POST, entity, TokenInfo.class); return response.getBody(); } }
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)