基于OAuth2.0的统一认证登录方案

Posted JAVA程序猿成长之路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于OAuth2.0的统一认证登录方案相关的知识,希望对你有一定的参考价值。

主要采用的授权模式的统一认证


授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。它的步骤如下:

(A)用户访问客户端,后者将前者导向认证服务器。

(B)用户选择是否给予客户端授权。

(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。

(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。

(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

下面是上面这些步骤所需要的参数。

A步骤中,客户端申请认证的URI,包含以下参数:

  • response_type:表示授权类型,必选项,此处的值固定为"code"

  • client_id:表示客户端的ID,必选项

  • redirect_uri:表示重定向URI,可选项

  • scope:表示申请的权限范围,可选项

  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

下面是一个例子。

GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
        &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1Host: server.example.com

C步骤中,服务器回应客户端的URI,包含以下参数:

  • code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。

  • state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。

下面是一个例子。

HTTP/1.1 302 FoundLocation: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
          &state=xyz

D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:

  • granttype:表示使用的授权模式,必选项,此处的值固定为"authorizationcode"。

  • code:表示上一步获得的授权码,必选项。

  • redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。

  • client_id:表示客户端ID,必选项。

下面是一个例子。

POST /token HTTP/1.1Host: server.example.comAuthorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JWContent-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

E步骤中,认证服务器发送的HTTP回复,包含以下参数:

  • access_token:表示访问令牌,必选项。

  • token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。

  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。

  • refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。

  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。

下面是一个例子。


     HTTP/1.1 200 OK
     Content-Type: application/json;charset=UTF-8
     Cache-Control: no-store
     Pragma: no-cache     {       "access_token":"2YotnFZFEjr1zCsicMWpAA",       "token_type":"example",       "expires_in":3600,       "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",       "example_parameter":"example_value"     }

从上面代码可以看到,相关参数使用JSON格式发送(Content-Type: application/json)。此外,HTTP头信息中明确指定不得缓存

案例主要采用SpringBoot+Spring Security+Spring OAuth2.0

下次代码主要介绍,如何在源码基础上返回更多的用户信息

 
   
   
 
  1. //

  2. // Source code recreated from a .class file by IntelliJ IDEA

  3. // (powered by Fernflower decompiler)

  4. //


  5. package org.springframework.boot.autoconfigure.security.oauth2.resource;


  6. import java.util.Collection;

  7. import java.util.Collections;

  8. import java.util.List;

  9. import java.util.Map;

  10. import java.util.Set;

  11. import org.apache.commons.logging.Log;

  12. import org.apache.commons.logging.LogFactory;

  13. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

  14. import org.springframework.security.core.AuthenticationException;

  15. import org.springframework.security.core.GrantedAuthority;

  16. import org.springframework.security.oauth2.client.OAuth2RestOperations;

  17. import org.springframework.security.oauth2.client.OAuth2RestTemplate;

  18. import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails;

  19. import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;

  20. import org.springframework.security.oauth2.common.OAuth2AccessToken;

  21. import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;

  22. import org.springframework.security.oauth2.provider.OAuth2Authentication;

  23. import org.springframework.security.oauth2.provider.OAuth2Request;

  24. import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;

  25. import org.springframework.util.Assert;


  26. public class UserInfoTokenServices implements ResourceServerTokenServices {

  27.    protected final Log logger = LogFactory.getLog(this.getClass());

  28.    private final String userInfoEndpointUrl;

  29.    private final String clientId;

  30.    private OAuth2RestOperations restTemplate;

  31.    private String tokenType = "Bearer";

  32.    private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();

  33.    private PrincipalExtractor principalExtractor = new FixedPrincipalExtractor();


  34.    public UserInfoTokenServices(String userInfoEndpointUrl, String clientId) {

  35.        this.userInfoEndpointUrl = userInfoEndpointUrl;

  36.        this.clientId = clientId;

  37.    }


  38.    public void setTokenType(String tokenType) {

  39.        this.tokenType = tokenType;

  40.    }


  41.    public void setRestTemplate(OAuth2RestOperations restTemplate) {

  42.        this.restTemplate = restTemplate;

  43.    }


  44.    public void setAuthoritiesExtractor(AuthoritiesExtractor authoritiesExtractor) {

  45.        Assert.notNull(authoritiesExtractor, "AuthoritiesExtractor must not be null");

  46.        this.authoritiesExtractor = authoritiesExtractor;

  47.    }


  48.    public void setPrincipalExtractor(PrincipalExtractor principalExtractor) {

  49.        Assert.notNull(principalExtractor, "PrincipalExtractor must not be null");

  50.        this.principalExtractor = principalExtractor;

  51.    }


  52.    /**

  53.     根据access_token获取用户认证信息(根据access_token调用认证服务器)

  54.    */

  55.    public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {

  56.        //this.userInfoEndpointUrl application.peoperties中配置的

  57.        //获取用户信息的URL security.oauth2.resource.userInfoUri

  58.        //accessToken 回去的access_token

  59.        //以下为根据access_token和获取用户信息的URL(需要则认证服务器写专门的controller处理)获取用户信息

  60.        Map<String, Object> map = this.getMap(this.userInfoEndpointUrl, accessToken);

  61.        if (map.containsKey("error")) {

  62.            if (this.logger.isDebugEnabled()) {

  63.                this.logger.debug("userinfo returned error: " + map.get("error"));

  64.            }


  65.            throw new InvalidTokenException(accessToken);

  66.        } else {

  67.            return this.extractAuthentication(map);

  68.        }

  69.    }


  70.    /**

  71.     提取用户认证信息

  72.    */

  73.    private OAuth2Authentication extractAuthentication(Map<String, Object> map) {

  74.        Object principal = this.getPrincipal(map);

  75.        List<GrantedAuthority> authorities = this.authoritiesExtractor.extractAuthorities(map);

  76.        OAuth2Request request = new OAuth2Request((Map)null, this.clientId, (Collection)null, true, (Set)null, (Set)null, (String)null, (Set)null, (Map)null);

  77.        //将提取的值principal作为构造函数参数

  78.        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);

  79.        token.setDetails(map);

  80.        return new OAuth2Authentication(request, token);

  81.    }

  82.     /**

  83.      从map中提取最终在UsernamePasswordAuthenticationToken构造函数中封装的值

  84.    */

  85.    protected Object getPrincipal(Map<String, Object> map) {

  86.        Object principal = this.principalExtractor.extractPrincipal(map);

  87.        return principal == null ? "unknown" : principal;

  88.    }


  89.    public OAuth2AccessToken readAccessToken(String accessToken) {

  90.        throw new UnsupportedOperationException("Not supported: read access token");

  91.    }


  92.   /**

  93.    调用认证服务器,获取用户信息

  94.   */

  95.    private Map<String, Object> getMap(String path, String accessToken) {

  96.        if (this.logger.isDebugEnabled()) {

  97.            this.logger.debug("Getting user info from: " + path);

  98.        }


  99.        try {

  100.            OAuth2RestOperations restTemplate = this.restTemplate;

  101.            if (restTemplate == null) {

  102.                BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();

  103.                resource.setClientId(this.clientId);

  104.                restTemplate = new OAuth2RestTemplate(resource);

  105.            }


  106.            OAuth2AccessToken existingToken = ((OAuth2RestOperations)restTemplate).getOAuth2ClientContext().getAccessToken();

  107.            if (existingToken == null || !accessToken.equals(existingToken.getValue())) {

  108.                DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(accessToken);

  109.                token.setTokenType(this.tokenType);

  110.                ((OAuth2RestOperations)restTemplate).getOAuth2ClientContext().setAccessToken(token);

  111.            }

  112.            //通过restTemplate返回用户信息

  113.            return (Map)((OAuth2RestOperations)restTemplate).getForEntity(path, Map.class, new Object[0]).getBody();

  114.        } catch (Exception var6) {

  115.            this.logger.warn("Could not fetch user details: " + var6.getClass() + ", " + var6.getMessage());

  116.            return Collections.singletonMap("error", "Could not fetch user details");

  117.        }

  118.    }

  119. }

`

 
   
   
 
  1. /**

  2. 最终在map中提取的是什么信息,封装在principal中

  3. 注意:以下只会返回一个合适的值,即使你认证服务返回在多的信息,最终都无法在客户端显示

  4. */

  5. public class FixedPrincipalExtractor implements PrincipalExtractor {


  6.    //

  7.    private static final String[] PRINCIPAL_KEYS = new String[] { "user", "username",

  8.            "userid", "user_id", "login", "id", "name" };


  9.    @Override

  10.    public Object extractPrincipal(Map<String, Object> map) {

  11.        //提取只会返回一个合适的值,最终封装在principal中

  12.        //主要还是看认证服务器返回用户信息的接口是否返回如上数组定义的字段信息,如果没有可能会返回null

  13.        for (String key : PRINCIPAL_KEYS) {

  14.            if (map.containsKey(key)) {

  15.                return map.get(key);

  16.            }

  17.        }

  18.        return null;

  19.    }


  20. }

解决方案

只需要将源码中的以下代码,替换为自己想要返回的参数

 
   
   
 
  1.    protected Object getPrincipal(Map<String, Object> map) {

  2.        Object principal = this.principalExtractor.extractPrincipal(map);

  3.        return principal == null ? "unknown" : principal;

  4.    }

自己实现的代码

 
   
   
 
  1.    protected Object getPrincipal(Map<String, Object> map) {


  2.        CustomPrincipal customPrincipal = new CustomPrincipal();

  3.        customPrincipal.setAccount((String) map.get("account"));

  4.        customPrincipal.setName((String) map.get("name"));

  5.        customPrincipal.setPhone((String) map.get("phone"));

  6.        //and so on..

  7.        return customPrincipal;


  8.        /*

  9.        Object principal = this.principalExtractor.extractPrincipal(map);

  10.        return (principal == null ? "unknown" : principal);

  11.        */


  12.    }

 
   
   
 
  1. import com.alibaba.fastjson.JSONObject;


  2. /**

  3. * @author Created by niugang on 2019/1/18/20:19

  4. */

  5. public class CustomPrincipal {


  6.    public CustomPrincipal() {

  7.    }

  8.    private String email;


  9.    private String name;


  10.    private String account;


  11.    private String phone;


  12.    //Getters and Setters

  13.        return email;

  14.    }


  15.        this.email = email;

  16.    }


  17.    public String getName() {

  18.        return name;

  19.    }


  20.    public void setName(String name) {

  21.        this.name = name;

  22.    }


  23.    public String getAccount() {

  24.        return account;

  25.    }


  26.    public void setAccount(String account) {

  27.        this.account = account;

  28.    }


  29.    public String getPhone() {

  30.        return phone;

  31.    }


  32.    public void setPhone(String phone) {

  33.        this.phone = phone;

  34.    }


  35.     /**

  36.     * 因为在principal.getName()返回的参数是字符串类型的,所以为了好处理我们需要用json用格式化一下

  37.     * @return String

  38.     */

  39.    @Override

  40.    public String toString() {

  41.        JSONObject jsonObject = new JSONObject();

  42.        jsonObject.put("email", this.email);

  43.        jsonObject.put("name", this.name);

  44.        jsonObject.put("account", this.account);

  45.        jsonObject.put("phone", this.phone);

  46.        return jsonObject.toString();



  47.    }

  48. }

前端获取用户新 ,注意这块中的是freemarker,采坑点好久没有写freemarker一直认为springboot默认 freemarker后缀为.html,结果一直不能返回到视图,最终看了源码默认后缀为.ftl

  • 总结:对于我们平常写代码,一丁点小知识点都不能忽略

前后调用接口获取需要显示的用户信息

 
   
   
 
  1. <script type="text/javascript">


  2.    $.get("/getUserInfo", function (data) {

  3.        if (data) {

  4.            $("#account").html(data.account);

  5.            $("#email").html(data.email);

  6.            $("#name").html(data.name);

  7.            $("#phone").html(data.phone);

  8.        }

  9.    });


  10. </script>

后台返回用户信息接口

 
   
   
 
  1.    /**

  2.     * @param principal 存在登录成功后的信息

  3.     * @return Object

  4.     */

  5.    @RequestMapping({"/getUserInfo"})

  6.    @ResponseBody

  7.    public Object user(Principal principal) {

  8.        if (principal != null) {

  9.            //重写之后 principal.getName()存放是 CustomPrincipal toString形式的数据

  10.            String jsonObject = principal.getName();

  11.            if (jsonObject != null) {

  12.                CustomPrincipal customPrincipal = JSON.parseObject(jsonObject, CustomPrincipal.class);

  13.                return customPrincipal;

  14.            }

  15.        }


  16.        return new Object();

  17.    }

注意:以上并非全部代码,只是重点核心代码,额外的配置参考

https://gitee.com/niugangxy/springcloudAction


以上是关于基于OAuth2.0的统一认证登录方案的主要内容,如果未能解决你的问题,请参考以下文章

如何在分布式环境中搭建单点登录系统| 第二篇:基于Oauth2.0开发SSO核心代码

IT新手之路关于oauth2.0的简单理解

统一认证授权及单点登录的技术选择

实战干货!Spring Cloud Gateway 整合 OAuth2.0 实现分布式统一认证授权!

最新版微服务架构鉴权解决方案Spring Cloud Gateway + Oauth2.0+mybatis+mysql+redis+nacos 统一认证和鉴权

技术干货 | OAuth2.0的安全解析