当前位置: 首页 > 工具软件 > oauth2-shiro > 使用案例 >

springboot-shiro多realm认证授权集成oauth2+jwt单点

蔡理
2023-12-01

总体思路:

单系统分密码登录与免密码登录(手机验证码,邮箱登录,微信oauth2接入,其他系统单点过来),用多realm实现

单系统无需登录接入前端系统(移动端,小程序,大屏等),用oauth2认证获取jwt无状态token,进行调用接口

多系统互相跳转实现单点,用oltu框架oauth2协议实现(同一)

单点流程梳理:

方案一(参考xboot):

  • 站点1发现用户未登录时,跳转至认证中心
  • 认证中心发现用户未登录(Cookie中没有认证记录),显示认证中心的登录页面
  • 用户输入账号密码登录,认证成功后,前端记录认证信息保存后端返回的accessToken令牌(可通过令牌获取对应用户名username),后端Redis存入access_token对应信息,其key为username:站点1的clientId,返回站点1的access_token
  • 认证中心携带access_token跳转至站点1的回调地址
  • 站点1前端获取到access_token令牌,记录已登录状态,每次请求携带access_token

  • 站点2发现用户未登录时,跳转至认证中心
  • 认证中心前端判断已认证授权过:从前端Cookie中取出存储的accessToken(可通过令牌获取对应用户名username)
  • 认证中心调用已授权验证接口,先验证accessToken是否有效,若有效,则后端Redis存入新站点的access_token信息,其key为username:站点2的clientId,返回站点2的access_token
  • 认证中心携带access_token跳转至站点2的回调地址
  • 站点2前端获取到access_token令牌,记录已登录状态,每次请求携带access_token

注销流程分析示例

  • 任一站点用户退出登录,通过Redis失效所有以username开头的键值对即可

实现细节补充

  • 认证中心可通过clientId可获取站点的基本信息数据

退出登录接口

  • /xboot/oauth2/logout
    • 需要携带主站的accessToken,通常为信任的内部站点中使用,将删除失效当前用户登录的accessToken以及当前用户授权第三方应用的access_token

方案二(自定义系统)oauth2 授权码模式:

  • 用户在主系统登录后,点击菜单跳转子系统认证接口(若主系统未登录进入子系统的认证接口会跳转主系统登录界面)
  • 子系统的认证接口判断cookies若有sso_access_token,则向主系统对access_token验签和获取用户信息(只有验签成功并且该token对应的用户是主系统当前登录的用户时才返回正确信息),调用shiro登录该用户,免密进入子系统
  • 子系统cookies若无sso_access_token,构造获取code的url,重定向到主系统,主系统会携带code回调到子系统的回调接口,然后构造请求用code换取access_token和用户信息,并保存access_token到cookies,调用shiro登录该用户,免密进入子系统
  • 退出系统只作用于当前系统,不影响主系统
package com.ruoyi.web.controller.system;

import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.utils.CookieUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.shiro.auth.LoginType;
import com.ruoyi.framework.shiro.auth.UserToken;
import org.apache.oltu.oauth2.client.OAuthClient;
import org.apache.oltu.oauth2.client.URLConnectionClient;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest;
import org.apache.oltu.oauth2.client.response.OAuthAccessTokenResponse;
import org.apache.oltu.oauth2.common.OAuth;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.types.GrantType;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * 单点登录验证
 *
 * @author ruoyi
 */

@Controller
@RequestMapping("/sso-client")
public class SysSSOLoginController {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Value("${sso.client.clientId}")
    private String clientId;
    @Value("${sso.client.clientSecret}")
    private String clientSecret;
    @Value("${sso.client.accessTokenUrl}")
    private String accessTokenUrl;
    @Value("${sso.client.verifyTokenUrl}")
    private String verifyTokenUrl;
    @Value("${sso.client.authorizeUrl}")
    private String authorizeUrl;
    @Value("${sso.client.redirectUrl}")
    private String redirectUrl;
    @Value("${sso.client.responseType}")
    private String responseType;

    /**
     * 单点登录入口
     *
     * @param request
     * @return
     */
    @GetMapping("/auth")
    public String ssoAuth(HttpServletRequest request) {
        String ssoAccessToken = CookieUtils.getCookie(request, "sso_access_token");
        if (StringUtils.isNotEmpty(ssoAccessToken)) {
            //已存在,去验证token获取用户信息直接登录
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("accessToken", ssoAccessToken);
            String result = HttpUtil.post(verifyTokenUrl, paramMap);
            logger.info("==> 验证ssoAccessToken返回值: " + result);
            if (StringUtils.isNotEmpty(result)) {
                JSONObject jsonObject = JSON.parseObject(result);
                if ("200".equals(jsonObject.getString("code"))) {
                    String loginName = jsonObject.getJSONObject("data").getString("username");
                    try {
                        login(loginName);
                        return "redirect:/index";
                    } catch (AuthenticationException e) {
                        return "error/unauth";
                    }
                }
            }
        }
        //不存在则获取单点授权码
        //配置请求参数,构建oauth2的请求。设置请求服务地址(authorizeUrl)、clientId、response_type、redirectUrl
        String requestUrl = null;
        try {
            OAuthClientRequest accessTokenRequest = OAuthClientRequest.authorizationLocation(authorizeUrl)
                    .setClientId(clientId)
                    .setResponseType(responseType)
                    .setRedirectURI(redirectUrl)
                    .buildQueryMessage();
            requestUrl = accessTokenRequest.getLocationUri();
        } catch (OAuthSystemException e) {
            e.printStackTrace();
        }
        logger.info("==> 客户端重定向到服务端获取auth_code: " + requestUrl);
        return "redirect:" + requestUrl;
    }

    /**
     * 单点登录回调
     *
     * @param request
     * @param response
     * @return
     */
    @RequestMapping("/callback")
    public String callback(HttpServletRequest request, HttpServletResponse response) throws OAuthProblemException, OAuthSystemException {
        String code = request.getParameter("code");
        logger.info("==> 服务端回调,获取的code:" + code);
        OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
        OAuthClientRequest accessTokenRequest = null;
        try {
            accessTokenRequest = OAuthClientRequest
                    .tokenLocation(accessTokenUrl)
                    .setGrantType(GrantType.AUTHORIZATION_CODE)
                    .setClientId(clientId)
                    .setClientSecret(clientSecret)
                    .setCode(code)
                    .setRedirectURI(redirectUrl)
                    .buildQueryMessage();
        } catch (OAuthSystemException e) {
            e.printStackTrace();
        }
        //去服务端请求access token,并返回响应
        OAuthAccessTokenResponse oAuthResponse = oAuthClient.accessToken(accessTokenRequest, OAuth.HttpMethod.POST);
        logger.info("==> 客户端根据 code值 " + code + " 到服务端获取的access_token为:" + JSON.toJSONString(oAuthResponse.getBody()));

        if (StringUtils.isNotEmpty(oAuthResponse.getAccessToken())) {
            //保存access_token到cookies
            CookieUtils.setCookie(response, "sso_access_token", oAuthResponse.getAccessToken());
            String username = JSON.parseObject(oAuthResponse.getParam("userInfo")).getString("loginName");
            UserToken token = new UserToken(username, LoginType.SSO);
            Subject subject = SecurityUtils.getSubject();
            try {
                subject.login(token);
                return "redirect:/index";
            } catch (AuthenticationException e) {
                return "error/unauth";
            }
        }
        return "error/unauth";
    }

    /**
     * shiro登录
     *
     * @param username
     * @throws AuthenticationException
     */
    private void login(String username) {
        UserToken token = new UserToken(username, LoginType.SSO);
        Subject subject = SecurityUtils.getSubject();
        subject.login(token);
    }

}

一.第三方授权登录

1.用户在前端页面发起微信登录

2.后端根据clientId和clientSecret以及自定义第三方来源标识组装授权url,让前端重定向该url

3.用户确定授权,第三方回调到后端接口

4.后端获取用户标识与用户信息存表或与系统用户表关联

5.完成认证颁发token
 

二.关注微信订阅号登录

1.用户在订阅号发送验证码关键字

2.后端接收到用户标识和用户信息,并存表,生成验证码(会过期)与用户绑定

3.用户在前端输入验证码登录

4.完成认证颁发token

三.手机号验证码登录

1.用户在前端输入手机号获取验证码

2.用户输入验证码登录

3.完成认证颁发token

四.单点登录

1.在单点认证中心通过认证

2.进入系统直接颁发token

五.移动端登录

1.用户在移动端输入账号密码

2.完成认证颁发jwt token

 类似资料: