zoukankan      html  css  js  c++  java
  • OAuth2、CAS单点登录

    一、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);
    }


    参考:https://my.oschina.net/u/2351011/blog/3058424
  • 相关阅读:
    jq绑定on事件无效
    数字以0补全
    redis常用操作
    mysql数据操作日常
    centos端口映射
    centos7防火墙操作
    mysql5.7order by问题
    centos无法上网解决方法
    面试题
    ztree 获取子节点所有父节点的name的拼接
  • 原文地址:https://www.cnblogs.com/aligege/p/13712399.html
Copyright © 2011-2022 走看看