1、OAuth2单点认证原理

基于OAuth2的认证方式包含四种,其中单点登录最常用的是授权码模式,其基本的认证过程如下:

  1. 用户访问业务应用,业务应用进行登录检查;
  2. 业务应用重定向到OAuth2认证服务器,调用获取授权码的认证接口;
  3. OAuth2认证服务负责判断登录状态,如果未登录,则跳转到统一认证登录页面,如果已经登录,则直接到步骤5;
  4. 用户输入用户名、密码进行授权确认;
  5. 授权成功后,OAuth2认证服务携带授权码,重定向到指定的回调地址;
  6. 跳转到业务应用后,业务应用收到授权码,然后携带授权码,换取OAuth2的授权token;
  7. OAuth2认证服务校验授权码成功,返回授权token;
  8. 业务应用携带授权token,调用OAuth2接口获取用户信息;
  9. OAuth2认证服务校验授权token成功,返回用户信息;
  10. 业务系统根据用户信息,创建本系统的登录信息,单点登录成功。

2、启动OAuth2服务

平台提供了一个基于开源项目“spring-security-oauth2-authorization-server”封装的OAuth2服务,工程名是“yuncheng-oauth-boot”。

2.1、配置数据库链接

  • 因OAuth2集成涉及多处配置,平台出厂已经默认配好,建议初学者第一次本地调试时,保持默认端口9000、服务根路径为空不变。后续有多处默认配置OAuth2服务地址是“ http://127.0.0.1:9000 ”。
  • 数据库链接与平台保持一致,读同一个数据库,这样就保证了OAuth2的用户信息与平台维护的用户信息一致。

2.2、配置用户登录标识

大部分情况下,系统使用username,即登录名,作为用户的唯一标识,唯一标识在OAuth2认证服务、平台服务、业务系统之间传递,完成单点登录认证。

如果需要使用手机号或者邮箱作为唯一标识,可以修改后端yml里的配置“yuncheng.loginNameType”,值范围包括:username,即登录名;phone,即手机号;email,即邮箱,不配置时,默认为“username”。

配置修改后,在OAuth2的登录页面,就可以使用对应的唯一标识进行登录了。

注意:如果配置了用户登录标识,那么,平台的yml文件,以及业务系统的集成代码(后面有个示例介绍业务系统如何集成),也需要使用相同的用户登录标识,保持一致。

2.3、启动服务

配置好yml文件的参数,启动OAuth2服务即可。

3、云程平台集成OAuth2服务

3.1、配置前端参数

在“public/config/bootConfig.js”文件中配置“VUE_APP_SSO”为“oauth2”,意为开启OAuth2单点登录功能。

开启后,需同时配置“VUE_APP_OAUTH2_URL”、“VUE_APP_OAUTH2_CLIENT_ID”、“VUE_APP_OAUTH2_SCOPE”的值,如果是本地调试,平台出厂已经默认配好。

参数说明:

  • VUE_APP_OAUTH2_URL:上一步启动的OAuth2的服务地址。
  • VUE_APP_OAUTH2_CLIENT_ID:在OAuth2注册的客户端ID,平台出厂脚本中默认有一条“yuncheng-client”的客户端配置,客户端ID需要结合第4部分的《启用OAuth2客户端注册模块》理解。
  • VUE_APP_OAUTH2_SCOPE:请求认证的权限范围,保持固定参数“openid”不变即可。

3.2、配置后端参数

在yml文件中配置“yuncheng.oauth2-client.default”的值,平台出厂已经默认配好,初学者第一次本地调试时,可以保持不变。

参数说明:

  • provider-uri:上一步启动的OAuth2的服务地址。
  • client-id:在OAuth2注册的客户端ID,平台出厂脚本中默认有一条“yuncheng-client”的客户端配置。
  • client-secret:在OAuth2注册的客户端ID的密钥。
  • redirect-uri:在OAuth2注册的客户端ID的重定向地址,也就是平台前端的访问地址,注意本地调试不要使用“localhost”,应使用“127.0.0.1”。

客户端ID、客户端ID密钥、客户端ID重定向地址,需要结合第4部分的《启用OAuth2客户端注册模块》理解。

3.3、配置用户登录标识

在启动OAuth2服务的时候,配置了用户登录标识,在云程平台的服务中,也需要配置与OAuth2一致的用户登录标识。

3.4、启动平台

配置好参数后,启动平台。

3.5、测试单点效果

访问平台,如果没有登录,系统会跳转到OAuth2的登录页面,账号、密码同平台一致,输入账号密码,登录成功后会跳转到平台首页。

4、启用OAuth2客户端注册模块

4.1、授权客户端注册菜单

平台提供了一个“客户端注册”模块,用于管理OAuth2的客户端数据,该模块的菜单默认是未授权的。

使用管理员账号进入平台控制台->角色授权->后台角色->后台管理员,勾中“配置管理”下的“客户端注册”菜单及其子菜单,点击保存,完成授权。

刷新页面,重新加载授权信息,就可以看到“配置管理”菜单下的“客户端注册”菜单了。

4.2、说明客户端注册参数

模块下有一条默认数据“yuncheng-client”,就是平台的默认配置中使用的OAuth2客户端ID,不要删除这条数据。

如果要变更默认配置,可以通过“修改密钥”按钮,修改客户端ID的密钥,通过“编辑”按钮,修改客户端ID的重定向地址、授权范围等信息,也可以点击“新增”,新注册一个客户端ID。

修改或新增后,再对应修改第二部分平台的默认配置,包括前端配置和后端配置,然后重启平台服务。

需要注意的是,密钥分为“加密”和“明文”两种方式,加密方式不可逆,如果配置加密方式,需要自己提前记录下密钥原文,防止丢失。

密钥前面的字符串“{noop}”等,是加密方式,不需要关心,配置到配置文件中的应该是密钥原文。

新注册一个客户端ID,为下一步的业务系统集成OAuth2提供客户端ID。

参数说明:

  • 客户端ID:client-id,客户端的唯一标识。
  • 密钥类型:分为加密和明文两种方式。
  • 密钥:密钥原文,如果密钥类型选择“加密”,需要提前记录下密钥原文,防止丢失。
  • 客户端名称:客户端中文名称。
  • 认证方式:单点登录保持默认即可。
  • 重定向地址:认证请求后的重定向地址,也就是业务服务的访问地址,与平台或业务服务发送认证请求时携带的redirect_uri参数一致。如果平台或业务服务地址变动了,这里也需要同步修改,业务服务的请求参数也需要同步修改,最终需要保持一致。
  • 授权范围:单点登录保持默认即可。

5、业务系统集成OAuth2

5.1、同步用户

必须完成与平台的用户同步,与OAuth2的用户保持一致,才能使用OAuth2的单点登录功能。

用户的唯一标识是可配置的,可以是username(登录名)、phone(手机号)、email(邮箱)中的一个,这三个关键字段各业务系统应该保持一致。

5.1.1、从平台同步

根据平台提供的接口,业务系统主动从平台拉取数据,完成用户同步。

1、获取所有平台用户

对应接口 :List<UserActorImpl> getAllUserList()

接口地址 : http://127.0.0.1:30001/api/system/sysOrgConver/getAllUserList (注:接口地址路径以实际为准)

请求类型 :GET

参数 :无

返回值 :HttpResult<List<UserActorImpl>>用户对象集合

返回值示例:

"code": 200, "message": "操作成功", "success": true, "timestamp": 1630752126366, "result": [{ "id": "1373536011387523073", "loginName": "admin", "name": "管理员", "createTime": 1577808000000, "deptId": "1373536559281065985", "deptCode": " A01A06", "orgCode": "A01A06", "deptName": "研发部", "email": "aaa@163.com", "phone": "13801066662", "weixin": "xxxxxx"

2、通过用户id获取用户信息

对应接口 :UserActorImpl getUserById(String userId)

接口地址 : http://127.0.0.1:30001/api/system/sysOrgConver/getUserById?userId=xxxxx (注:接口地址路径以实际为准)

请求类型 :GET

参数 :userId

返回值 :HttpResult<UserActorImpl>(用户对象)

返回值示例:

"code": 200, "message": "操作成功", "success": true, "timestamp": 1630752126366, "result": { "id": "1373536011387523073", "loginName": "admin", "name": "管理员", "createTime": 1577808000000, "deptId": "1373536559281065985", "deptCode": " A01A06", "orgCode": "A01A06", "deptName": "研发部", "email": "aaa@163.com", "phone": "13801066662", "weixin": "xxxxxx"

5.1.2、从钉钉同步

平台完成了钉钉与平台的用户同步功能,可以参考文档: https://yunchengxc.yuque.com/staff-kxgs7i/public/ir11upm4igg0egr1#OLjQt ,结合钉钉官方API文档,自行开发钉钉用户同步功能。

5.2、改造代码完成集成

为了便于理解,我们选取一个基于SpringMVC开发的开源工程作为示例,讲解OAuth2的集成过程和原理,如果您的系统不是SpringMVC的技术栈,您也可以参考这个思路,完成自己代码的OAuth2集成。

5.2.1、引入Jar包

集成OAuth2的代码改造中,引入了4个工具Jar包,分别是JSON解析工具fastjson、远程调用工具httpclient和httpcore、jwt解析工具java-jwt。如果您的项目中已经有类似功能的工具包,也可以使用。

5.2.2、改造登录拦截逻辑

下图是SpringMVC的拦截器,主要做了两部分改造。

  • 红框1:定义排除拦截的请求,原逻辑是排除登录请求,改为排除“/oauth2.action”,“/oauth2.action”是业务系统提供给OAuth2的回调地址,也就是客户端配置的重定向地址。下一步有这个服务的具体实现示例。
  • 红框2:如果拦截器判断用户未登录,原逻辑是跳转到本系统的登录页面,改为跳转到OAuth2认证页面。具体逻辑见下图,其中使用的参数的含义后面会说明。这里需要增加一个session属性记录要访问的地址,当认证成功后,系统跳转时,可以从session中拿到该地址,进行跳转,保证业务请求是连贯的。

请求OAuth2需要携带一些参数,可以定义一个参数常量类,也可以使用配置文件配置的方式,本例使用了常量类,定义的参数含义说明如下:

  • clientId: 客户端ID。
  • clientSecret:客户端ID的密钥。
  • redirectUri:客户端定义的重定向地址。
  • providerUri:OAuth2的服务地址
  • tokenUri:换取token请求路径,固定成“/oauth2/token”。
  • authorizeUri:权限认证的请求路径,固定成“/oauth2/authorize”。
  • logoutUri:退出请求路径,固定成“/logout”。
  • loginNameType:单点登录身份标识类型,配置哪个属性标识唯一用户,默认为username,值范围包括: username(登录名)、phone(手机号)、email(邮箱)。

这些参数的值与第3部分注册的客户端对应。

下面是截图涉及的源码,供参考。

* 登录拦截器 public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 获取请求的URL String url = request.getRequestURI(); // 注释掉原来的不拦截逻辑 // if (url.indexOf("/login.action") >= 0) { // return true; // } // 不拦截oauth2的回调请求 if (url.indexOf("/oauth2.action") >= 0) { return true; // 获取Session HttpSession session = request.getSession(); User user = (User) session.getAttribute("USER_SESSION"); // 判断Session中是否有用户数据,如果有,则返回true,继续向下执行 if (user != null) { return true; // 注释掉原来的登录跳转逻辑,改为跳转到oauth2单点登录页面 // request.setAttribute("msg", "您还没有登录,请先登录!"); // request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response); // 记录当前请求地址,oauth2认证通过后调用回调地址时,会使用这个记录,跳转到用户想要访问的页面 String contextPath = request.getContextPath(); String nextUrl = url; if (contextPath != null && !contextPath.equals("")) { nextUrl = nextUrl.substring(request.getContextPath().length()); session.setAttribute("nextUrl", nextUrl); // 跳转到oauth2的登录页面 String oauth2Url = Oauth2Constant.providerUri + Oauth2Constant.authorizeUri; oauth2Url += "?response_type=code"; oauth2Url += "&client_id=" + Oauth2Constant.clientId; oauth2Url += "&scope=openid"; oauth2Url += "&redirect_uri=" + Oauth2Constant.redirectUri; response.sendRedirect(oauth2Url); return false; @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { * oauth2参数定义 public class Oauth2Constant { // 客户端ID public static final String clientId = "crm-client"; // 密钥 public static final String clientSecret = "crm-client"; // 重定向地址 public static final String redirectUri = "http://127.0.0.1:8090/crm/oauth2.action"; // oauth2服务地址 public static final String providerUri = "http://127.0.0.1:9000"; // 换取token请求路径 public static final String tokenUri = "/oauth2/token"; // 权限认证的请求路径 public static final String authorizeUri = "/oauth2/authorize"; // 退出请求路径 public static final String logoutUri = "/logout"; // 单点登录身份标识类型,配置哪个属性标识唯一用户,默认为username // 值范围: username:登录名;phone:手机号;email:邮箱 public static final String loginNameType = "username";

5.2.3、集成OAuth2认证服务

OAuth2认证通过后,会携带授权码重定向到业务系统指定服务地址,也就是上一步请求地址的“redirectUri”参数指定的地址,业务系统需要实现该服务,接收授权码,然后携带授权码,换取OAuth2的授权token,完成本系统的登录。对应第1部分原理图的步骤6-步骤10。

  • 红框1:参数校验,如果code为空,则跳转到登录页。一般退出操作导致的重定向是没有code参数的。
  • 红框2:使用code获取OAuth2的userToken,使用jwt解析工具类,从token中解析出用户唯一标识,这里使用了自己封装的一个工具类“Oauth2RestClient”,后面有源码供参考。
  • 红框3:根据配置的用户唯一标识的类型,调用本系统对应的的获取用户接口,获得用户对象。
  • 红框4:如果用户存在,则创建本系统的session,完成登录,如果用户不存在,则跳转OAuth2的退出页面,参数含义可以参考上一部分的参数含义说明。登录成功后的跳转地址,原逻辑是跳转到首页,这里需要从session记录里取到认证前要访问的地址,然后跳转到该地址。

下图是自己封装的一个工具类“Oauth2RestClient”。

下面是截图涉及的源码,供参考。

@Controller
public class Oauth2Controller {
    @Resource
    private UserService userService;
     * oauth2重定向地址,即回调地址
    @RequestMapping(value = "/oauth2.action")
    public String oauth2(@RequestParam(name = "code", required = false) String code, HttpSession session) throws IOException {
        if (code == null || "".equals(code)) {
            // 如果参数不全,返回到登录页面
            // 推出之后,会跳转回该地址,此时没有code,需要校验
            return "redirect:login.action";
        // 初始化工具类
        Oauth2RestClient oauth2RestClient = new Oauth2RestClient();
        // 换取accessToken
        UserToken userToken = oauth2RestClient.validCode(code);
        // 获取用户名
        String username = oauth2RestClient.getUserName(userToken);
        // 查询用户
        User user = null;
        if ("phone".equals(Oauth2Constant.loginNameType)) {
            user = userService.findUserByPhone(username);
        } else if ("email".equals(Oauth2Constant.loginNameType)) {
            user = userService.findUserByEmail(username);
        } else {
            user = userService.findUserByUserCode(username);
        if (user != null) {
            // 将用户对象添加到Session
            session.setAttribute("USER_SESSION", user);
            // 注释掉原来的跳转到主页面的逻辑
//            return "redirect:customer/list.action";
            // 改为从记录中拿到认证前想要访问的请求地址,进行跳转
            String url = (String) session.getAttribute("nextUrl");
            return "redirect:" + url;
        // 如果登录失败,重定向到oauth2的退出页面
        String oauth2Url = Oauth2Constant.providerUri + Oauth2Constant.logoutUri;
        oauth2Url += "?redirect_uri=" + Oauth2Constant.redirectUri;
        return "redirect:" + oauth2Url;
 * oauth2客户端工具类
public class Oauth2RestClient {
    public UserToken validCode(String code) throws IOException {
        String url = Oauth2Constant.providerUri + Oauth2Constant.tokenUri;
        Map<String, String> map = new HashMap<>();
        map.put("grant_type", "authorization_code");
        map.put("client_id", Oauth2Constant.clientId);
        map.put("client_secret", Oauth2Constant.clientSecret);
        map.put("redirect_uri", Oauth2Constant.redirectUri);
        map.put("code", code);
        URI uri = this.createURI(url, map);
        return this.doPost(uri).toJavaObject(UserToken.class);
    public String getUserName(UserToken userToken) {
        DecodedJWT jwt = JWT.decode(userToken.getAccessToken());
        return jwt.getSubject();
    private URI createURI(String url, Map<String, String> map) {
        int loop = 1;
        for (Map.Entry<String, String> entry : map.entrySet()) {
            if (StringUtils.isNotEmpty(entry.getValue())) {
                url += loop == 1 ? "?" : "&";
                url += entry.getKey() + "=" + entry.getValue();
                loop++;
        return URI.create(url);
    private JSONObject doPost(URI uri) throws IOException {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(uri);
        CloseableHttpResponse response = httpClient.execute(httpPost);
        String resultString = EntityUtils.toString(response.getEntity(), "utf-8");
        return JSON.parseObject(resultString);
 * oauth2对象
public class UserToken {
    private String accessToken;
    private String refreshToken;
    private String tokenType;
    private String scope;
    private String idToken;
    public String getAccessToken() {
        return accessToken;
    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    public String getRefreshToken() {
        return refreshToken;
    public void setRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    public String getTokenType() {
        return tokenType;
    public void setTokenType(String tokenType) {
        this.tokenType = tokenType;
    public String getScope() {
        return scope;
    public void setScope(String scope) {
        this.scope = scope;
    public String getIdToken() {
        return idToken;
    public void setIdToken(String idToken) {
        this.idToken = idToken;

5.2.4、改造退出功能

原退出逻辑是销毁session,跳转到本系统登录页面,改为销毁session,跳转到OAuth2的退出页面,参数含义可以参考上一部分的参数含义说明。

下面是截图涉及的源码,供参考。

* 退出登录 @RequestMapping(value = "/logout.action") public String logout(HttpSession session) { // 清除Session session.invalidate(); // 重定向到oauth2的退出页面 String oauth2Url = Oauth2Constant.providerUri + Oauth2Constant.logoutUri; oauth2Url += "?redirect_uri=" + Oauth2Constant.redirectUri; return "redirect:" + oauth2Url; // 重定向到登录页面的跳转方法 // return "redirect:login.action";

5.3、测试单点效果

如下图,可以在平台的菜单中,配置一个集成好的业务系统的访问地址。完成菜单授权,刷新页面。

在平台与OAuth2集成、业务系统与OAuth2集成都正确的情况下,通过OAuth2认证页面登录平台后,在菜单中访问第三方业务系统,无需登录即可直接打开,即完成了单点登录集成。

@EnableOAuth2Client 客户端,提供OAuth2RestTemplate,用于客户端访问资源服务。 简要步骤:客户端访问资源->客户端发现没有资源访问token->客户端根据授权类型生成跳转url->浏览器 302 到认证授权服务进行认证、授权。 @EnableOAuth2Sso 应用系统,使用远端认证授权服务,替换应用自身的用户登录鉴权securit
Jeecg是一款开源的快速开发平台,可以帮助开发者快速构建企业级应用。实现单点登录是Jeecg的一个重要功能之一。 在Jeecg中,可以通过Shiro插件实现单点登录。Shiro是一个强大的Java安全框架,可以处理身份验证、授权、会话管理和密码加密等安全相关的功能。 首先,需要在Jeecg项目中引入Shiro插件。可以通过在pom.xml文件中添加相关依赖来引入Shiro,然后配置Shiro的相关参数。在配置文件中,需要设置身份验证和授权的规则,以及会话管理相关的配置。 接下来,需要在Jeecg项目中配置一个认证地址,用于登录处理。可以在Web层的Controller中添加一个方法来处理用户登录请求,并将用户的登录信息和密码进行身份验证。如果验证成功,可以生成一个包含用户信息的token,并重定向到指定的URL。 在其他需要登录验证的系统中,也需要引入Shiro插件,并配置相关参数。同时,需要在相关的Controller中添加一个方法,用于接收认证地址返回的token。通过验证token的有效性,可以验证用户的身份,并进行相应的授权和权限管理操作。 这样,Jeecg就实现了单点登录的功能。当用户在一个系统中进行登录后,可以在其他需要登录验证的系统中直接访问,无需重新验证身份。这大大提高了用户使用系统的便利性和效率。 总结来说,Jeecg通过引入Shiro插件,并配置相关参数,可以实现单点登录的功能。这样的实现方式可以提高用户体验,同时也增加了系统的安全性。