在手撕之前,你首先要了解一些原理,我写的案例成品可以访问zauth,语言是Java8。
目录
1、关于Http
2、用户信息怎么存?存什么?存在哪?
2.1 使用前端存储技术Storage或indexedDB
2.2 前端使用Cookie
2.3 后台使用Session
2.4 使用token
2.5 token规范之JWT
2.6 番外篇,教你CSRF攻击简单实战。
2.7 Oauth认证服务
2.7.1 Access token
2.7.2 Refresh token?
?2.7.3 教你如何接入第三方认证服务 :Gitee为例
3、跨域问题
3.1 JSONP或iframe
3.2 使用ticket解决跨域问题
1、关于Http
????????大家可能都会看到这样的面试题:在浏览器的地址栏输入网址,回车后会发生什么事?当然我们今天不是讨论这个的,但是你首先要知道后台是如何感知谁登录的系统。我们知道http是无状态的,通俗讲,就是它本身是不去记录xxx浏览了什么文档或者网址,更不去记录xxx刚发了http请求。http请求方和响应方间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。
????????那么懂了,当你登录系统的时候,之后的请求需要前端手动把用户信息通过http请求带到后台,之所以用“带”这个字,因为“带”的方式有很多。
2、用户信息怎么存?存什么?存在哪?
2.1 使用前端存储技术Storage或indexedDB
????????首先,这种设计思路就是:你用户一登录,后台先去校验该用户是否存在,如果存在,就会返回用户所有信息,前端存入Storage中。之后每次需要携带用户信息请求的ajax,都会在请求前从Storage中取用户信息,可以选择带到请求头里。后台通过校验请求头,就知道该操作是谁发起的。
? ? ? ? 且先不谈合不合理,目前这种设计应用还是比较多,因为用户信息由前端保存,这是很多前端框架乐于看到的,这个用户信息也可以加密,能封装和拓展的点有很多。某些管理系统,内网项目愿意这样设计(仅前台),因为简单,仅面向部分人,又不太用考虑恶意用户的情况。
? ? ? ? 这样设计的优点:1、简单易懂,集成方便,能有更多的时间去开发业务代码,如果你后台代码写的好,能够让业务功能跟鉴权功能解耦。2、不可被爬虫。3、多用户的情况下可以直接覆盖。
? ? ? ? 这样设计的缺点:1、需要浏览器支持Storage。2、时效得单独管理,因为Storage的有效期。3、用户信息容易被“专业”人士看到(可以加密)。4、每次都要在请求头携带大量的用户信息(明文或加密),比较冗余而且浪费流量。5、请求头的内容容易被截取到,防止不了伪造的请求。6、域名限制。
使用Storage存储确实也有不少问题,优化点也非常多,仅Storage的时效就难以管理,需要前后端都去维护这个过期问题。Cookie就能有效解决这个问题。
2.2 前端使用Cookie
? ? ? ? 首先,Cookie的创建可以在前端,也可以在后台。它跟Storage一样,都是存在浏览器上的。通过地址栏发起的请求是自动携带的,ajax得去手动设置该请求需要携带cookie。也可以做到前端完全无感知。
? ? ? ? Cookie是生命周期的,是由我们去创建的,所以完全不必考虑用户登录时效的问题。存储的内容我们就先不存用户信息了,而是先引入我们第三个兄弟Session。
2.3 后台使用Session
? ? ? ? 我们登录之后呢,可以把用户信息存入session中,然后生成一个sessionId,它的生命周期也是我们设置的,是一次“会话”。这个sessionId存在哪呢?可以是数据库,也可以是redis,但是我们先把它存到Cookie内。有趣的是,这个sessionId是会自动种到Cookie里的。(下图的JSESSIONID就是,图片来自于我的博客网站)
? ? ? ? ?sessionId就是会话ID,用它去判断当前用户对应哪个Session。但是坑爹的是,这个Session是存在服务端的内存里,如果很多用户用这一套登录系统是不能容忍的。这套设计我建议直接pass,想都不要想。session可能在rpc框架或者消息队列中间件里有奇效,因为这些功能生命周期可以设计的很短并可以马上销毁,但是你用来登录认证就是在挑战面试官的极限。
?????????这样设计的缺点(不要再想优点了):1、session存在服务器内存里,用户多的话就会给服务器造成压力。2、session拓展性不高,面对集群得去考虑更多问题,因为用户登录通过负载均衡打在A服务器上,A服务器就会保存session,第二次请求打在B服务器上,B服务器可没有这个session啊,就会判断该请求用户登录已过期。有人说他会用分布式session,他真的很杠。3、cookie的内容是可以被截获的,防止不了跨站点伪造请求。
我很建议session就不用考虑了,但是session确实是一种思路,我们取其精华去其糟粕,不用session保存用户信息,也不用生成sessionId。我们使用redis去保存用户信息,使用UUID作为redis的Key,我们管这个key叫token。
2.4 使用token
? ? ? ? 基于session版的改进,用户登录先去数据库校验用户是否存在,然后取用户信息到redis里面,我们会生成一个UUID作为保存该用户信息到redis的key,然后把该key种cookie中,每次校验cookie携带的key是否存在,或者该key是否能查询到redis的用户信息,就能完美解决用户登录鉴权问题。这就是zauth v1.0版本的设计,也是一个网站登录系统最简单的一种思路。
????????这样设计的优点:1、解决了上面用户信息存在哪里的困扰,不必考虑分布式或集群的问题。2、通过管理redis为token设置有效期,不必再去考虑cookie的问题,你只需要把token写到cookie里面就行了。
? ? ? ? 这样设计的缺点:1、cookie里的token依然能被截获,依旧解决不了跨站点伪造请求。
2.5 token规范之JWT
? ? ? ? jwt就是JSON Web Token的简称,是一种开放标志,一个规范吧。如果你要自己做一个登录或者权限认证的框架,可以选择不用或不了解,UUID生成的token就已经够用。如果你要引入一些中间件Spring Security或Apache?shiro,都会在教你如何整合JWT。那么我们也去了解一下(规则访问官网)。
? ? ? ? ?上图就是jwt的内容,它由三部分组成,分别是header头部,payload载荷部分以及signature签名部分。根据官网,我们可以看到:头部主要是类型以及加密算法的声明,然后通过base64编码形成jwt的第一部分;载荷我们可以存取用户信息,然后通过base64编码形成jwt中间部分;尾部是将base64编码的头部和编码后的载荷部分,中间以.拼接起来之后,通过定义的加密算法(HS256)经签名后加密的字符串,最后三部分用.拼起来就是具有jwt规范的token。有人会问:都知道你载荷用base64编码了,我拿到cookie里的token然后把两个.中间的载荷取出来用base64解码不就拿到用户信息了?确实,但是它不是用来防止伪造请求的,而是用来防止篡改,这就是签名的作用,不如先看看下面代码。
先引入pom依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
直接上代码:?
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.*;
import org.junit.Test;
import java.util.Date;
import java.util.UUID;
/**
* @author: zq
* jwt测试
*/
public class JWTDemo {
//签名
private final static String signature="zhangqi";
//jwt加密
@Test
public void jwtEncode(){
JSONObject user=new JSONObject();//用户信息
user.put("id","1");user.put("username","zhangqi");user.put("image","dignitas.png");
//构建jwt构建器
JwtBuilder jwtBuilder= Jwts.builder();
String jwtToken=jwtBuilder
//头信息,header
.setHeaderParam("type","jwt")
.setHeaderParam("alg","HS256")
//载荷信息,payload
.claim("user",user.toJSONString())
//设置主题 可选
.setSubject("userTest")
//有效时间 我设置了7天 从当前时间戳加上7天
.setExpiration(new Date(System.currentTimeMillis()+1000*60*60*24*7))
//设置jwt的ID 我用了UUID
.setId(UUID.randomUUID().toString())
//签名 声明签名算法跟签名,我用了HS256
.signWith(SignatureAlgorithm.HS256,signature)
//构建jwt,返回token字符串
.compact();
System.out.println(jwtToken);
}
@Test
public void jwtDecode(){
String jwtToken="eyJ0eXBlIjoiand0IiwiYWxnIjoiSFMyNTYifQ" +
".eyJ1c2VyIjoie1wiaW1hZ2VcIjpcImRpZ25pdGFzLnBuZ1wiLFwiaWRcIjpcIjFcIixcInVzZXJuYW1lXCI6XCJ6aGFuZ3FpXCJ9Iiwic3ViIjoidXNlclRlc3QiLCJleHAiOjE2NDYxMjUyNDIsImp0aSI6IjNlOTlmY2I3LWY3YmYtNDgzNC1hMDJiLWY0ZTZjMzc5NjkyOSJ9" +
".m30L8nkFoL-S8vGfLJW6a4MUrJRk94ZIuYlJH0Iq_uo";
JwtParser jwtParser=Jwts.parser();
//传入签名,对jwtToken解密
Jws<Claims> claimsJws=jwtParser.setSigningKey(signature).parseClaimsJws(jwtToken);
Claims claims=claimsJws.getBody();
System.out.println("jwt载荷信息(用户信息):"+claims.get("user"));
System.out.println("jwtID:"+claims.getId());
System.out.println("jwt主题:"+claims.getSubject());
System.out.println("jwt有效时间:"+claims.getExpiration());
}
}
?执行结果:
? ? ? ? ?下面我们再来说说为什么jwt的主要功能是防止篡改,首先可以看到,我们可以在jwt里设置jwt有效时间,并把用户信息写入载荷部分,这个设置的有效时间也是被编进jwtToken里的,攻击者也是能看到的。如果他修改了jwtToken的有效时间,使之无限延长,并且伪造请求携带此token,但是他不知道我们的签名是什么。也就是他无法修改jwtToken的签名部分,他只能知道我们使用了HS256去加密的前两部分,而不知道secret,也就是签名(代码里的zhangqi,私钥),所以根本无法篡改jwt的内容。有人迷了,我tm才不管能不能篡改呢,他什么都不改不就一样可以伪造请求了?所以,再强调一点,jwt只是一种token规范。至于如何解决伪造请求,这个得前后端一起努力(之后会讲到)。这样说吧,payload里存的东西是由我们后台去写入的,担心写入明文被解码的话,你可以在base64之前自己加盐加密一遍payload里的信息。你也完全可以多加一个时间戳防止重复请求。jwt只管token无法被篡改,不解决其他功能。
? ? ? ? 使用jwt规范的缺点:1、你要注意一点,签名(私钥)保存好,不要像我demo那样硬编码进代码里,搞不好你就提交到GitHub上去了,可以存在数据库或者配置文件里,不要提交就好了。2、如果通过jwt的载荷去保存用户信息,那么每次请求携带的cookie都会很大,浪费流量。当然你可以存redis的Key,不过直接用一个UUID生成的token不香吗?3、编解码都是一笔开销。
2.6 番外篇,教你CSRF攻击简单实战。
在阅读上面内容中,大家都会发现以上都无法防止csrf攻击。csrf就是跨站点伪造请求,它的原理很简单,无非就是截获你登录成功后的token,然后使用该token去伪造请求。
? ? ? ? 大家都或许听说过QQ盗号,或者Steam的API劫持然后就被盗库存了。都有一个相同点,就是在QQ、QQ邮箱、或者Steam里误点链接。
? ? ? ? 拿Steam为例,如果你在Steam里点了链接,那么它就充当套壳浏览器的作用,使用它的内核,它先在它的域名下携带该链接,然后重定向到链接所在的网站。由于Steam跟QQ是登录才能用的,不经意间,人家就拿到你登录的token,就可以为所欲为了,当然这招已经不好使了。第二种就是人家伪造一个很真的页面,它先通过抓取工具拿到登录接口,然后你在它的页面登录,那么登录用户的Cookie就会很自然的被种入它的域名下。
? ? ? ? 看个例子,大家可以访问一下我的这个网站,然后登录。后果是你的关注名单里会多了一个小可爱。然后你每个粉丝都会收到一条“你好!”的消息。当然这些功能已经取消了,被网易云警告了。事实上,如果你很专业,你也可以选择不登录,在网易云网页的cookie里拿到这个_csrf,然后手动种到下面这个网站并刷新一下也能达到相同的效果。
? ? ? ? 假如你还是不想登录,没关系,这个_csrf默认用我自己的,你可以看看我收藏的歌单,右边给我推送的歌单,或者给某些评论点点赞然后打开你手机的网易云APP看看赞是不是+1了呢?这些操作都是用户在登录状态下才有效的。也就是说,不经意间,我通过登录接口种入cookie,然后伪造了这些请求。
? ?不如直接看看代码,非常简单:? ? ???????所以,不要点击不明链接,不要轻易在其他网站登录,不过你在谷歌浏览器无痕模式还是能看看人家链接。无论怎样,核心原理都是如何获取标志用户登录的token。你就揪住这点,一抓一个准,防不胜防。交流加我微信HOPPIN_HAZZ ,有QQ跟Steam的盗号实战,别干坏事哦。
2.7 Oauth认证服务
这是第三方认证服务,就是QQ,微信这种,你的系统可以通过它们提供的第三方认证服务访问,如果你没有这方面的需求,可以跳过,因为下面都是我自己设计的,不能保证安全或者专业。
须知:这是后台与第三方认证服务的调用。
2.7.1 Access token
? ? ? ? accessToken你就把它当做token使用就行了,一般需要验证权限的接口就携带这个token就行了。为什么叫access token呢,为了跟下面refresh token对比。你先记住,业务验证权限是去拿access token去进行鉴权,而且它的有效期非常短,所以比较安全而且伸缩性更强。有的同学会有疑问,很短的话,是不是基本每个操作都要去登录一遍才行?No
2.7.2 Refresh token?
? ? ? ? 上面说了,access token有效期很短,系统很容易就会因为access token失效而重定向登录页去了。但是这样体验非常不友好,就设计了一个refresh token解决这个问题,相比access token,refresh token有效期较长,而且它不参与鉴权,它只干一件事:生成access token。先看一下现在的流程。
? ? ? ? 首先,这是A系统要接入你的第三方系统要操作的,你只需要让他们重定向到你的登录页,然后走你的登录流程,重定向到授权页,用户一旦授权后,将access token跟refresh token返回给A系统,至于这两个token如何保存是A系统的问题。A系统通过传入access token去调用你提供的获取当前登录人的接口,来获取登录人用户信息以便它写入它的数据库。
?2.7.3 教你如何接入第三方认证服务 :Gitee为例
QQ,微博接入都一样。微信我没接入过,因为不提供个人接入,你可以去淘宝买企业接入,就是两个秘钥。接入之后你不仅可以直接获取第三方的用户信息,还可以通过api去操作很多事情,比如Gitee接入可以让用户授权你更多权限,你可以用他提供给你的权限帮他提交代码,给别人点星星之类的;可以直接从你网站分享个性链接装饰,而不是一个单调的网站链接。
1、访问Gitee的第三方应用页面
?2、都填一填,就这么简单?
3、模拟测试一下,大功告成?
?4、没这么简单!没错,你只是重定向到了,但是谁跳过去的你并不知道,也是说用户信息你连拿都没拿到。这就需要我们流程图里的,调用第三方提供的接口了,gitee的oauth接口访问文档。
5、代码我就不写了,我是通过HttpCilent模拟Http请求调用的Gitee接口,大家自己去拉代码。
3、跨域问题
跨域是单点登录必须要考虑的问题且是最核心的问题,必须要熟知跨域在sso是如何产生的
?????????首先先明确你要做的单点登录系统是不是跟你的系统部署在同一台服务器上用的同一个域名。如果你想做的简单一点,那么token那节其实就够用。一般而言,当我们做大量的项目的时候,只希望有一套单点登录系统,而单点登录系统跟你部署的系统一般是不在同一个域名下,这时就要考虑跨域问题。
? ? ? ? 首先,先说明一下在我的这个设计里跨域是如何产生的,从上面可以看到,我暂时是用cookie存的token,那么它就携带以下请求头。
?1、Domain跟Path :Domain是域名,Path是路径。Domain属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前 URL 的一级域名,而且以后如果访问其任何子域名,HTTP 请求也会带上这个 Cookie。如果服务器在Set-Cookie字段指定的域名,不属于当前域名,浏览器会拒绝这个 Cookie。
????????Path属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如,PATH属性是/,那么请求/index.html路径也会包含该 Cookie。当然,前提是域名必须一致。
? ? ? ??这个就是跨域问题产生的原因,我的zauth单点登录服务部署在150.158.28.40服务器上,而博客是部署在1.15.232.156服务器上。按照我们之前设计的流程,携带token的cookie会被写入150.158.28.40的IP下,1.15.232.156是无法读取到该cookie的。
2、Expires / Max-Age
? ? ? ? 没什么好说的,就是过期时间。值得注意的是,如果没有指定过期时间,那么这个cookie就是session cookie(Max-Age内容是session的),生命周期就是浏览器的一次会话,就跟SessionStorage一样,浏览器关闭就无了。
3、Secure / HttpOnly
????????大家可以看到内容是√,首先Secure打√的,说明该cookie只能在加密协议HTTPS下使用并携带到后台,在HTTP协议下是获取不到的。
????????HttpOnly打√的,说明该cookie只能通过HTTP请求携带,使用JavaScript是无法获取到的。上面我们说了,Cookie是前端后台都能生成,在前端是通过document.cookie来操作cookie,但是加了这个头之后,该Cookie就无法在前端js里操作了。
?可以看到,跨域问题主要是Domain配置不是同一个域名导致的。
? ? ? ? ?据我观察一些sso开源项目,都会考虑至少以下三点:1、服务同域,redis同源。2、服务不同域,redis同源。3、服务不同域,redis不同源。有人对2可能不是很理解,其实现在一些公司都有一套单独的redis集群只去管理用户信息,我就采用了2的设计,先只考虑最难的服务不同域。
? ? ? ? 将服务不同域的问题转化一下,就变成如何把sso域下的token种到业务模块域下。有两种方法:
3.1 JSONP或iframe
? ? ? ? 我们知道,在前端,带有src属性的标签具有天然可以处理跨域的能力,比如引入一张图片<img src="xxx.com/xxx.jpg"/> ,甚至你可以把百度通过iframe嵌入你的网站<iframe src="https://www.baidu.com/" ></iframe>.,然后利用iframe与父页面的通讯来操作。那么就很简单了。你的想法非常好,但是遗憾的是,不允许在iframe里去操作cookie。
? ? ? ? 上面的做法直接pass,我们再来看看JSONP能不能解决问题。
????????我就不买关子了,我很负责任的跟你讲,JSONP是一定能解决跨域问题的,但是需要sso提供JSONP接口。由于JSONP比较low,而且有安全隐患,我们也不考虑。?
3.2 使用ticket解决跨域问题
? ? ? ? 这就是最终的解决方法,刚来个token又来个ticket,这个ticket其实就是一个sso系统签发的凭证,业务系统根据这个凭证,就能拿到token。直接上网图,懒得画了:
zauth流程是这样的:?
?????????首先用户先访问我们业务模块,如果用户访问的业务模块需要登录权限,那就会被重定向到sso并携带重定向前的url。因为用户在sso登录成功后要能重定向回之前的操作页面,就需要携带这个url。比如用户想要访问的博客页面,并要写博客(写博客访问http://1.15.232.156/writeblog.html)。但是由于写博客需要登录,所以用户没有登录过的话(token没有或无效)访问这个页面会被后台重定向到sso系统http://150.158.28.40:8804/login.html?cz___zauth=1&redirect=http://1.15.232.156/writeblog.html??
? ? ? ? 当用户在sso系统验证身份成功后,需要重定向回写博客页面,并携带sso签发的ticket,http://1.15.232.156/writeblog.html?ucode=0a470b6677004b83a94fe2a9ba8be4e9,为了保证这个ticket(ucode)足够安全,通常在使用过一次后就无效了。以上都是我们在链接上耍的小心思,再看看代码:
页面鉴权及重定向:
/**
* 页面跳转通用接口
* 1、首页:index.html
* @param url
* @return
*/
@RequestMapping("{url}.html")
public String page(@PathVariable("url") String url, String ucode,HttpServletRequest request, HttpServletResponse response) throws IOException {
logger.debug("------------------------------------------------");
logger.debug("要访问"+url+".html页面了");
try{
if(needLoginWebUrl.indexOf(url)!=-1){
logger.debug("访问的"+url+".html页面需要登录权限");
logger.debug("连接auth服务开始");
UserPrincipal upp = new UserPrincipal(rpcPropertyBean.getUserName(), rpcPropertyBean.getPassword());
LoginService loginService= ServiceProxyFactory.createProxy(LoginService.class, rpcPropertyBean.getServerAuth(), upp);
logger.debug("连接auth服务成功");
String token = CookieUtils.getCookieValue(request,"ZQ_TOKEN");
logger.debug("获取到了ZQ_TOKEN的Cookie的token:"+token);
if(token==null&&ucode!=null){
logger.debug("获取到了一次性ucode:"+ucode);
User user =loginService.getUserByCode(ucode);
if(user==null){
token=null;
}else{
logger.debug("查询到了ucode对应的用户信息:"+JSONObject.toJSONString(user));
token=user.getToken();
logger.debug("查询到了对应的用户信息的token:"+token);
//设置token有效期
logger.debug("写入值为:BLOG:USER:"+token+"redis中");
redisUtils.set("BLOG:USER:"+token,user,7*24*60*60);
logger.debug("写入值为:ZQ_TOKEN的Cookie中:"+token);
Cookie cookie = new Cookie("ZQ_TOKEN", token);
cookie.setMaxAge(60*60*24*7);
//cookie.setDomain(mainUrl);
cookie.setPath("/");
response.addCookie(cookie);
logger.debug("返回"+url+".html页面中");
return url+".html";
}
}
if (null == token) {
logger.debug("没有获取到token,将重定向至:"+authUrl + "?cz___zauth=1&redirect=" + request.getRequestURL());
response.sendRedirect(authUrl + "?cz___zauth=1&redirect=" + request.getRequestURL());
}else{
//应该去auth服务里查询用户,但是返回的是null
User user=(User)redisUtils.get("BLOG:USER:"+token);
//User user = loginService.getUserByToken("BLOG:USER:"+token);
if(null==user){
logger.debug("获取到的token没有查询到用户,表示这个token:"+token+"已过期");
//清空cookie
logger.debug("清空这个过期的token");
Cookie cookie = new Cookie("ZQ_TOKEN", "");
cookie.setMaxAge(0);
response.addCookie(cookie);
logger.debug("重定向至:"+authUrl + "?cz___zauth=1&redirect=" + request.getRequestURL());
response.sendRedirect(authUrl + "?cz___zauth=1&redirect=" + request.getRequestURL());
}
if(needMemberWebUrl.indexOf(url)!=-1){
logger.debug("访问的"+url+".html页面需要会员权限");
if(user.getUserright()!=1){
logger.debug("获取到的用户没有会员权限");
logger.debug("重定向至:"+authUrl + "?cz___zauth=2&redirect=" + request.getRequestURL());
response.sendRedirect(authUrl + "?cz___zauth=2&redirect=" + request.getRequestURL());
}
}
if(needAdminWebUrl.indexOf(url)!=-1){
logger.debug("访问的"+url+".html页面需要管理员权限");
if(user.getUserright()!=2){
logger.debug("获取到的用户没有管理员权限");
logger.debug("重定向至:"+authUrl + "?cz___zauth=3&redirect=" + request.getRequestURL());
response.sendRedirect(authUrl + "?cz___zauth=3&redirect=" + request.getRequestURL());
}
}
}
}else{
//在本页面登录
if(ucode!=null){
logger.debug("获取到了一次性ucode:"+ucode);
UserPrincipal upp = new UserPrincipal(rpcPropertyBean.getUserName(), rpcPropertyBean.getPassword());
LoginService loginService= ServiceProxyFactory.createProxy(LoginService.class, rpcPropertyBean.getServerAuth(), upp);
User user =loginService.getUserByCode(ucode);
String token;
if(user==null){
token=null;
}else{
logger.debug("查询到了ucode对应的用户信息:"+JSONObject.toJSONString(user));
token=user.getToken();
logger.debug("查询到了对应的用户信息的token:"+token);
//设置token有效期
logger.debug("写入值为:BLOG:USER:"+token+"redis中");
redisUtils.set("BLOG:USER:"+token,user,7*24*60*60);
logger.debug("写入值为:ZQ_TOKEN的Cookie中:"+token);
Cookie cookie = new Cookie("ZQ_TOKEN", token);
cookie.setMaxAge(60*60*24*7);
//cookie.setDomain(mainUrl);
cookie.setPath("/");
response.addCookie(cookie);
logger.debug("返回"+url+".html页面中");
return url+".html";
}
}
}
}catch (Exception ex){
logger.error("登入"+url+"出现异常!");
}finally {
logger.debug("返回"+url+".html页面中");
logger.debug("------------------------------------------------");
return url+".html";
}
}
接口鉴权及重定向:
/**
* 权限校验
* 重写该方法以实现自己的权限校验
* 返回true校验通过
* 该方法需要注册中心和auth服务,耦合非常高,你可以通过继承本类,并重写rightCheck方法解耦
* 但是我的所有模块都是用的这个auth服务去做的用户认证,为了避免代码重复,就写在这里了
* @param request
* @param response
* @return
* @throws IOException
*/
public Boolean rightCheck(HttpServletRequest request,HttpServletResponse response,RequestParam requestParam) throws IOException{
if(apiPropertyBean.isAuth()){
return true;
}
LoginUser.enter();
ServiceMethodApiBean serviceMethodApiBean=requestParam.getApiRunnable().getServiceMethodApiBean();
if(serviceMethodApiBean.methodRight != ApiMapping.RoleType.NO_RIGHT){
String token=requestParam.getToken()==null?CookieUtils.getCookieValue(request,"ZQ_TOKEN")
:requestParam.getToken();
if(null==token){
redirectUrl(request,response);
return false;
}
//UserPrincipal upp = new UserPrincipal(rpcPropertyBean.getUserName(), rpcPropertyBean.getPassword());
//LoginService loginService= ServiceProxyFactory.createProxy(LoginService.class, rpcPropertyBean.getServerAuth(), upp);
//User user = loginService.getUserByToken("BLOG:USER:"+token);
User user=(User)redisUtils.get("BLOG:USER:"+token);
if (null == user) {
Cookie cookie = new Cookie("ZQ_TOKEN", "");
cookie.setMaxAge(0);
response.addCookie(cookie);
redirectUrl(request,response);
return false;
}else{
if(serviceMethodApiBean.methodRight== ApiMapping.RoleType.ADMIN&&user.getUserright()!=1){
redirectUrl(request,response);
return false;
}
}
JSONObject userJSON= JSONObject.parseObject(JSON.toJSONString(user));
request.setAttribute("user", userJSON);
LoginUser.setUserHold(userJSON);
}
return true;
}
/**
* 重定向url
* @param request
* @param response
* @throws IOException
*/
private void redirectUrl(HttpServletRequest request,HttpServletResponse response) throws IOException {
RequestParam requestParam= (RequestParam)RequestContext.getPrincipal();
ServiceMethodApiBean serviceMethodApiBean=requestParam.getApiRunnable().getServiceMethodApiBean();
//Ajax请求
if("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))){
String sourceUrl=request.getHeader("Referer");
if(null==sourceUrl){
sourceUrl=request.getRequestURL().toString();
}
if(serviceMethodApiBean.methodRight== ApiMapping.RoleType.ADMIN){
response.setHeader("redirect", apiPropertyBean.getSsoAdminUrl() + "?redirect=" +sourceUrl);
}else{
response.setHeader("redirect", apiPropertyBean.getSsoUrl() + "?redirect=" +sourceUrl);
}
response.setHeader("enableRedirect","true");
response.addHeader("Access-Control-Expose-Headers","redirect,enableRedirect,isAdmin");
response.setStatus(302);
response.flushBuffer();
}
//浏览器地址栏请求
else {
String queryString =request.getQueryString();
String requestURL=String.valueOf(request.getRequestURL());
String realUrl=requestURL+"?"+queryString;
//跳转到登录页面,把用户请求的url作为参数传递给登录页面。
if(serviceMethodApiBean.methodRight== ApiMapping.RoleType.ADMIN){
response.sendRedirect(apiPropertyBean.getSsoAdminUrl() + "?redirect=" + realUrl);
}else{
response.sendRedirect(apiPropertyBean.getSsoUrl() + "?redirect=" + realUrl);
}
}
}
额,代码大家可能难以理解,下一篇将详细讲解细节。
|