OAuth2、CAS单点登录

原创
2019/06/04 18:23
阅读数 4.5W

一、Oauth 是一个关于授权(authorization)的开网络标准(规范)

OAuth2: 解决的是不同的企业之间的登录,本质是授权,如论坛与QQ

要能访问各种资源重点是要获取令牌(token),但根据令牌的获取方式不同,又会有四种授权方式

  1. 授权码(authorization-code)
  2. 隐藏式(implicit)
  3. 密码式(password)
  4. 客户端凭证(client credentials)

授权码这是最常用的一种方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌,项目中用的就是这种

隐藏式:允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit),一般应用于纯前端项目

密码式:直接通过用户名和密码的方式申请令牌,这方式是最不安全的方式

凭证式这种方式的令牌是针对第三方应用,而不是针对用户的,既某个第三方应用的所有用户共用一个令牌,一般用于没有前端的命令行应用

授权码授权流程:

第一步,A 网站提供一个链接,用户点击后就会跳转到 B 网站(权限验证系统)

http://b.com/oauth/authorize?

  response_type=code&

  client_id=CLIENT_ID&

  redirect_uri=CALLBACK_URL&

  scope=read

用户跳转后,B 网站如果没有登录会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri参数指定的网址,并附加授权码code

http://a.com/callback?code=AUTHORIZATION_CODE

第三步,A 网站拿到授权码以后,在后端,向 B 网站请求令牌。

http://b.com/oauth/token?

 client_id=CLIENT_ID&

 client_secret=CLIENT_SECRET&

 grant_type=authorization_code&

 code=AUTHORIZATION_CODE&

 redirect_uri=CALLBACK_URL

上面 URL 中,client_id参数和client_secret参数用来让 B 确认 A 的身份(client_secret参数是保密的,因此只能在后端发请求),grant_type参数的值是AUTHORIZATION_CODE,表示采用的授权方式是授权码,code参数是上一步拿到的授权码,redirect_uri参数是令牌颁发后的回调网址。

第四步,B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据。

{    

  "access_token":"ACCESS_TOKEN",

  "info":{...}

}

接下来用户就可以根据这个access_token来进行访问了,

如A网站拿着token,申请获取用户信息,B网站确认令牌无误,同意向A网站开放资源。

对于第三方网站来说 可分为3部分

1、申请code

2、申请token

3、带着token去请求资源(如:申请获取用户信息)

伪代码

服务端

@RequestMapping("authorize")
public Object authorize(Model model, HttpServletRequest request) throws OAuthSystemException, URISyntaxException {
    //构建OAuth请求
    OAuthAuthzRequest oAuthAuthzRequest = null;
    try {
        oAuthAuthzRequest = new OAuthAuthzRequest(request);

        // 根据传入的clientId 判断 客户端是否存在
        if(!authorizeService.checkClientId(oAuthAuthzRequest.getClientId())) {
            return HttpResponseBody.failResponse("客户端验证失败,如错误的client_id/client_secret");
        }

        // 判断用户是否登录
        Subject subject = SecurityUtils.getSubject();

        if(!subject.isAuthenticated()) {
            if(!login(subject, request)) {
                return new HttpResponseBody(ResponseCodeConstant.UN_LOGIN_ERROR, "没有登陆");
            }
        }
        String username = (String) subject.getPrincipal();

        //生成授权码
        String authorizationCode = null;

        String responseType = oAuthAuthzRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE);
        if(responseType.equals(ResponseType.CODE.toString())) {
            OAuthIssuerImpl oAuthIssuer = new OAuthIssuerImpl(new MD5Generator());
            authorizationCode = oAuthIssuer.authorizationCode();
            shiroCacheUtil.addAuthCode(authorizationCode, username);
        }

        Map<String, Object> data = new HashMap<>();
        data.put(SsoConstants.AUTH_CODE, authorizationCode);
        return HttpResponseBody.successResponse("ok", data);

    } catch(OAuthProblemException e) {
        return HttpResponseBody.failResponse(e.getMessage());
    }
}
@RequestMapping("/accessToken")
public HttpEntity token(HttpServletRequest request) throws OAuthSystemException {
    try {
        // 构建Oauth请求
        OAuthTokenRequest oAuthTokenRequest = new OAuthTokenRequest(request);
        
        //检查提交的客户端id是否正确
        if(!authorizeService.checkClientId(oAuthTokenRequest.getClientId())) {
            OAuthResponse response = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                    .setError(OAuthError.TokenResponse.INVALID_CLIENT)
                    .setErrorDescription("客户端验证失败,如错误的client_id/client_secret")
                    .buildJSONMessage();
            return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
        }
        
        // 检查客户端安全Key是否正确
        if(!authorizeService.checkClientSecret(oAuthTokenRequest.getClientSecret())){
            OAuthResponse response = OAuthASResponse.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
                    .setError(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT)
                    .setErrorDescription("客户端验证失败,如错误的client_id/client_secret")
                    .buildJSONMessage();
            return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
        }
        
        String authCode = oAuthTokenRequest.getParam(OAuth.OAUTH_CODE);
        
        // 检查验证类型,此处只检查AUTHORIZATION类型,其他的还有PASSWORD或者REFRESH_TOKEN
        if(oAuthTokenRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(GrantType.AUTHORIZATION_CODE.toString())){
            if(!shiroCacheUtil.checkAuthCode(authCode)){
                OAuthResponse response = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                        .setError(OAuthError.TokenResponse.INVALID_GRANT)
                        .setErrorDescription("error grant code")
                        .buildJSONMessage();
                return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
            }
        }

        //生成Access Token
        OAuthIssuer issuer = new OAuthIssuerImpl(new MD5Generator());
        final String accessToken  = issuer.accessToken();
        shiroCacheUtil.addAccessToken(accessToken, shiroCacheUtil.getUsernameByAuthCode(authCode));
        logger.info("accessToken generated : {}", accessToken);

        //需要保存clientSessionId和clientId的关系到redis,便于在Logout时通知系统logout
        String clientSessionId = request.getParameter("sid");
        //System.out.println("clientSessionId = " + clientSessionId);
        String clientId = oAuthTokenRequest.getClientId();
        //System.out.println("clientId = " + clientId);
        redisTemplate.opsForHash().put(RedisKey.CLIENT_SESSIONS, clientSessionId, clientId);

        // 生成OAuth响应
        OAuthResponse response = OAuthASResponse.tokenResponse(HttpServletResponse.SC_OK)
                .setAccessToken(accessToken).setExpiresIn(String.valueOf(authorizeService.getExpireIn()))
                .buildJSONMessage();
        
        return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
    } catch(OAuthProblemException e) {
        e.printStackTrace();
            OAuthResponse res = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e).buildBodyMessage();
        return new ResponseEntity<>(res.getBody(), HttpStatus.valueOf(res.getResponseStatus()));
    }
}
@RequestMapping("/userInfo")
public HttpEntity userInfo(HttpServletRequest request) throws OAuthSystemException {
    try {
        
        //构建OAuth资源请求
        OAuthAccessResourceRequest oauthRequest = new OAuthAccessResourceRequest(request, ParameterStyle.QUERY);
        //获取Access Token
        String accessToken = oauthRequest.getAccessToken();

        //验证Access Token
        if (!shiroCacheUtil.checkAccessToken(accessToken)) {
            // 如果不存在/过期了,返回未验证错误,需重新验证
            OAuthResponse oauthResponse = OAuthRSResponse
                    .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
                    .setRealm("fxb")
                    .setError(OAuthError.ResourceResponse.INVALID_TOKEN)
                    .buildHeaderMessage();
            
            HttpHeaders headers = new HttpHeaders();
            headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
            return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED);
        }
        //返回用户名
        String username = shiroCacheUtil.getUsernameByAccessToken(accessToken);
        SysUser user = userService.selectByAccount(username);
        return new ResponseEntity<>(user, HttpStatus.OK);
    } catch (OAuthProblemException e) {
        //检查是否设置了错误码
        String errorCode = e.getError();
        if (OAuthUtils.isEmpty(errorCode)) {
            OAuthResponse oauthResponse = OAuthRSResponse
                    .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
                    .setRealm("fxb")
                    .buildHeaderMessage();
            
            HttpHeaders headers = new HttpHeaders();
            headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
            return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED);
        }
        
        OAuthResponse oauthResponse = OAuthRSResponse
                .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
                .setRealm("fxb")
                .setError(e.getError())
                .setErrorDescription(e.getDescription())
                .setErrorUri(e.getUri())
                .buildHeaderMessage();
        
        HttpHeaders headers = new HttpHeaders();
        headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
        return new ResponseEntity(HttpStatus.BAD_REQUEST);
    }
}

客户端

private String extractUsername(String code) {
    OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
    try {
        OAuthClientRequest accessTokenRequest = OAuthClientRequest.tokenLocation(accessTokenUrl)
                .setGrantType(GrantType.AUTHORIZATION_CODE)
                .setClientId(clientId)
                .setClientSecret(clientSecret)
                .setCode(code)
                .setRedirectURI(redirectUrl)
                .setParameter("sid", SecurityUtils.getSubject().getSession().getId().toString())
                .buildQueryMessage();
        OAuthAccessTokenResponse oAuthResponse = oAuthClient.accessToken(accessTokenRequest, OAuth.HttpMethod.POST);
        String accessToken = oAuthResponse.getAccessToken();

        //拿用户信息
        OAuthClientRequest userInfoRequest = new OAuthBearerClientRequest(userInfoUrl)
                .setAccessToken(accessToken).buildQueryMessage();

        OAuthResourceResponse resourceResponse = oAuthClient.resource(userInfoRequest, OAuth.HttpMethod.GET, OAuthResourceResponse.class);

        String userJson = resourceResponse.getBody();
        SysUser user = JsonUtils.json2Obj(userJson, SysUser.class);
        this.setResource(user, accessToken);
        return user.getUserName();
    } catch(OAuthSystemException e) {
        e.printStackTrace();
        throw new  RuntimeException(e);
    } catch(OAuthProblemException e) {
        e.printStackTrace();
        throw new BusinessException(ResponseCodeConstant.UN_LOGIN_ERROR, "没有登录");
    }
}
<dependency>
   <groupId>org.apache.oltu.oauth2</groupId>
   <artifactId>org.apache.oltu.oauth2.authzserver</artifactId>
   <version>1.0.2</version>
</dependency>
<dependency>
   <groupId>org.apache.oltu.oauth2</groupId>
   <artifactId>org.apache.oltu.oauth2.resourceserver</artifactId>
   <version>1.0.2</version>
</dependency>

二、单点: 是解决企业内部的一系列产品登录问题,安全信任度要比oauth2高

(一)session-cookie机制

1、session-cookie机制出现的根源, http连接是无状态的连接

-------- 同一浏览器向服务端发送多次请求,服务器无法识别,哪些请求是同一个浏览器发出的

2、为了标识哪些请求是属于同一个人 ---------- 需要在请求里加一个标识参数

方法1-----------直接在url里加一个标识参数(对前端开发有侵入性),如: token

方法2-----------http请求时,自动携带浏览器的cookie(对前端开发无知觉),如:jsessionid=XXXXXXX

3、浏览器标识在网络上的传输,是明文的,不安全的

-----------安全措施:改https来保障

4、cookie的使用限制---依赖域名

-------------- 顶级域名下cookie,会被二级以下的域名请求,自动携带

-------------- 二级域名的cookie,不能携带被其它域名下的请求携带

5、在服务器后台,通过解读标识信息(token或jsessionid),来对应会话是哪个session

--------------- 一个tomcat,被1000个用户登陆,tomcat里一定有1000个session -------》存储格式map《sessionid,session对象》

--------------- 通过前端传递的jsessionid,来对应取的session ------ 动作发生时机request.getsession

(二)session共享方式,实现的单点登陆

1、多个应用共用同一个顶级域名,sessionid被种在顶级域名的cookie里

2、后台session通过redis实现共享(重写httprequest、httpsession 或使用springsession框架),即每个tomcat都在请求开始时,到redis查询session;在请求返回时,将自身session对象存入redis

3、当请求到达服务器时,服务器直接解读cookie中的sessionid,然后通过sessionid到redis中查找到对应会话session对象

4、后台判断请求是否已登陆,主要校验session对象中,是否存在登陆用户信息

5、整个校验过程,通过filter过滤器来拦截切入,如下图:

6、登陆成功时,后台需要给页面种cookie方法如下:

response里,反映的种cookie效果如下:

7、为了request.getsession时,自动能拿到redis中共享的session,

    我们需要重写request的getsession方法(使用HttpServletRequestWrapper包装原request)

(三)cas单点登陆方案

1、对于完全不同域名的系统,cookie是无法跨域名共享的

2、cas方案,直接启用一个专业的用来登陆的域名(比如:cas.com)来供所有的系统登陆。

3、当业务系统(如b.com)被打开时,借助cas系统来登陆,过程如下:

cas登陆的全过程:

(1)、b.com打开时,发现自己未登陆 ----》 于是跳转到cas.com去登陆

(2)、cas.com登陆页面被打开,用户输入帐户/密码登陆成功

(3)、cas.com登陆成功,种cookie到cas.com域名下 -----------》把sessionid放入后台redis《ticket,sesssionid》---页面跳回b.com

String ticket = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(ticket,request.getSession().getId(),20, TimeUnit.SECONDS);//一定要设置过期时间
CookieBasedSession.onNewSession(request,response);
response.sendRedirect(user.getBackurl()+"?ticket="+ticket);

(4)、b.com重新被打开,发现仍然是未登陆,但是有了一个ticket值

(5)、b.com用ticket值,到redis里查到sessionid,并做session同步 ------ 》种cookie给自己,页面原地重跳

(6)、b.com打开自己页面,此时有了cookie,后台校验登陆状态,成功

(7)整个过程交互,列图如下:

4、cas.com的登陆页面被打开时,如果此时cas.com本来就是登陆状态的,则自动返回生成ticket给业务系统

整个单点登陆的关键部位,是利用cas.com的cookie保持cas.com是登陆状态,此后任何第三个系统跳入,都将自动完成登陆过程

5,本示例中,使用了redis来做cas的服务接口,请根据工作情况,自行替换为合适的服务接口(主要是根据sessionid来判断用户是否已登陆)

6,为提高安全性,ticket应该使用过即作废(本例中,会用有效期机制)

public void doFilter(ServletRequest servletRequest,ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    MyRequestWrapper myRequestWrapper = new MyRequestWrapper(request,redisTemplate);
    //如果未登陆状态,进入下面逻辑
    String requestUrl = request.getServletPath();
    if (!"/toLogin".equals(requestUrl) && !requestUrl.startsWith("/login")  && !myRequestWrapper.isLogin()) {
        /**
         * ticket为空,或无对应sessionid为空
         * --- 表明不是自动登陆请求--直接强制到登陆页面
         */
        String ticket = request.getParameter("ticket");
        if (null == ticket || null == redisTemplate.opsForValue().get(ticket)){
            HttpServletResponse response = (HttpServletResponse)servletResponse;
            response.sendRedirect("http://cas.com:8090/toLogin?url="+request.getRequestURL().toString());
            return ;
        }
        /**
         * 是自动登陆请求,则种cookie值进去---本次请求是302重定向
         * 重定向后的下次请求,自带本cookie,将直接是登陆状态
         */
        myRequestWrapper.setSessionId((String) redisTemplate.opsForValue().get(ticket));
        myRequestWrapper.createSession();
        //种cookie
        CookieBasedSession.onNewSession(myRequestWrapper,(HttpServletResponse)servletResponse);
        //重定向自流转一次,原地跳转重向一次
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        response.sendRedirect(request.getRequestURL().toString());
        return;
    }
    try {
        filterChain.doFilter(myRequestWrapper,servletResponse);
    } finally {
        myRequestWrapper.commitSession();
    }
}
public static void onNewSession(HttpServletRequest request, HttpServletResponse response) {
    HttpSession session = request.getSession();
    String sessionId = session.getId();
    Cookie cookie = new Cookie(COOKIE_NAME_SESSION, sessionId);
    cookie.setHttpOnly(true);
    cookie.setPath(request.getContextPath() + "/");
    cookie.setMaxAge(Integer.MAX_VALUE);
    response.addCookie(cookie);
}
展开阅读全文
打赏
4
1 收藏
分享
加载中
不知大佬源码地址在哪里
2020/12/17 14:02
回复
举报
更多评论
打赏
1 评论
1 收藏
4
分享
返回顶部
顶部