OAuth2, CAS single sign-on

1. Oauth is an open network standard (specification) about authorization (authorization)

OAuth2 : It solves the login between different enterprises, the essence is authorization , such as forums and QQ

To be able to access various resources, the key is to obtain tokens (tokens), but depending on how tokens are obtained, there are four authorization methods

  1. Authorization code (authorization-code)
  2. hidden (implicit)
  3. password
  4. client credentials

Authorization code: This is the most commonly used method, which means that a third-party application first applies for an authorization code, and then uses this code to obtain a token. This is what is used in the project

Stealth: Allows issuing tokens directly to the frontend. This method does not have the intermediate step of authorization code, so it is called (authorization code) "implicit" (implicit), generally applied to pure front-end projects

Password: Apply for tokens directly through username and password, which is the least secure way

Credential type: This type of token is for third-party applications, not for users. That is, all users of a third-party application share a token, which is generally used for command-line applications without front-ends

Authorization code authorization process:

In the first step, website A provides a link, and the user will jump to website B (authority verification system) after clicking

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

  response_type=code&

  client_id=CLIENT_ID&

  redirect_uri=CALLBACK_URL&

  scope=read

In the second step, after the user jumps, if the website B has not logged in, it will ask the user to log in, and then ask whether to agree to authorize the website A. If the user agrees, then website B will jump back to the URL specified by the redirect_uri parameter and append the authorization code code

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

In the third step, after website A gets the authorization code, it requests a token from website B at the backend.

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

Guess you like

Origin blog.csdn.net/shark1357/article/details/108745016