打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
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);}
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Cookie、Session、Token 解析
从session、cookie到token以及JWT
公司系统太多,能不能实现账号互通?
分布式中使用Redis实现Session共享(二)
Cookie、Session、Token 的区别
redis实现分布式集群环境session共享(代码示例与验证)
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服