sso简介
百度百科
什么是单点登录(What)
单点登录( Single Sign-On , 简称 SSO )是目前比较流行的服务于企业登录业务整合的解决方案之一, SSO 使得在多个应用系统中,用户只需要 登录一次 就可以访问所有相互信任的应用系统。
比如:
Qq qq空间 qq游戏 qq邮箱
百度 百度百科 百度贴吧 百度网盘
为什么要使用sso(Why)
我们有多个前端站点,有多个站点是需要登录才能够访问的,不可能所有站点都要写一个登录,需要一个站点登录了其他站点就不需要登录了
方案设计(How)
方案1:依赖于一些权限框架 shiro security
方案2:用一个单点登录框架 cas
方案3:自己设计,直接写(知其然亦知其所以然)
原来的登录
微服务
代码实现(Just do it)
实现步骤:
- 后端服务保护处理-zuul拦截
- 登录
1 后端登录服务
2 前端登录实现
- 站点做登录检查
首先我们需要定义一个拦截器 将没有登录的请求全部拦截下来
位置
放到zuul 网关模块中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64 1package org.leryoo.filter;
2
3import com.netflix.zuul.ZuulFilter;
4import com.netflix.zuul.context.RequestContext;
5import com.netflix.zuul.exception.ZuulException;
6import org.springframework.http.HttpStatus;
7import org.springframework.stereotype.Component;
8
9import javax.servlet.http.HttpServletRequest;
10
11@Component
12public class LoginFilter extends ZuulFilter {
13
14
15 @Override
16 public String filterType() {
17 // 登录校验,肯定是在前置拦截
18 return "pre";
19 }
20
21 @Override
22 public int filterOrder() {
23 // 顺序设置为1
24 return 1;
25 }
26
27 //登录放行
28 @Override
29 public boolean shouldFilter() {
30 // 返回true,代表过滤器生效。
31 return true;
32 }
33
34
35 @Override
36 public Object run() throws ZuulException {
37 // 登录校验逻辑。
38 // 1)获取Zuul提供的请求上下文对象
39 RequestContext ctx = RequestContext.getCurrentContext();
40 // 2) 从上下文中获取request对象
41 HttpServletRequest req = ctx.getRequest();
42
43 //对登录放行
44 String requestURI = req.getRequestURI();
45 if (requestURI.contains("login"))
46 return null;
47 if (requestURI.contains("api-docs"))
48 return null;
49 // 3) 从请求中获取token
50 String token = req.getHeader("access-token");
51
52 // 4) 判断
53 if(token == null || "".equals(token.trim())){
54 // 没有token,登录校验失败,拦截
55 ctx.setSendZuulResponse(false);
56 // 返回401状态码。也可以考虑重定向到登录页。
57 ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
58 }
59 // 校验通过,可以考虑把用户信息放入上下文,继续向后执行
60 return null;
61 }
62}
63
64
说明 这里需要注意的是 对登录页面和swagger界面进行一个放行
然后还有需要对注册界面的一个获取图片验证码,手机验证码的请求进行一个放行
然后是登录的Controller层
1
2
3
4
5
6
7 1//登录
2@PostMapping("/login")
3public AjaxResult login(@RequestBody Sso sso){
4 return ssoService.login(sso);
5}
6
7
Service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 1@Override
2public AjaxResult login(Sso sso) {
3 //校验用户是否存在,状态ok,是否已经过期等待
4 if (!StringUtils.hasLength(sso.getPhone()) || !StringUtils.hasLength(sso.getPassword()))
5 return AjaxResult.me().setSuccess(false).setMessage("请输入用户名或密码!");
6
7 List<Sso> ssoList = ssoMapper.selectList(new EntityWrapper<Sso>()
8 .eq("phone", sso.getPhone()));
9 if (ssoList==null || ssoList.size()<1)
10 return AjaxResult.me().setSuccess(false).setMessage("用户不存在,请注册后再来登录!");
11
12 //从数据库查询sso
13 Sso ssoExsit = ssoList.get(0);
14
15 //进行密码比对-输入密码+数据库盐值=md5再和数据库密码比对
16 System.out.println(sso.getPassword());
17 System.out.println(ssoExsit.getSalt());
18 String md5Pwd = MD5.getMD5(sso.getPassword() + ssoExsit.getSalt());
19 System.out.println(md5Pwd);
20 if (!ssoExsit.getPassword().equals(md5Pwd)){
21 return AjaxResult.me().setSuccess(false).setMessage("请输入正确的用户名或密码!");
22 }
23
24 //用户存到redis并且返回token-60*30
25 String accessToken = UUID.randomUUID().toString();
26 redisClient.addForTime(accessToken, JSONObject.toJSONString(ssoExsit),30*60);
27 return AjaxResult.me().setResultObj(accessToken);
28}
29
30
工具类
随机
StrUtils.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93 1package org.leryoo.util;
2
3import java.util.ArrayList;
4import java.util.List;
5import java.util.Random;
6
7public class StrUtils {
8 /**
9 * 把逗号分隔的字符串转换字符串数组
10 *
11 * @param str
12 * @return
13 */
14 public static String[] splitStr2StrArr(String str,String split) {
15 if (str != null && !str.equals("")) {
16 return str.split(split);
17 }
18 return null;
19 }
20
21
22 /**
23 * 把逗号分隔字符串转换List的Long
24 *
25 * @param str
26 * @return
27 */
28 public static List<Long> splitStr2LongArr(String str) {
29 String[] strings = splitStr2StrArr(str,",");
30 if (strings == null) return null;
31
32 List<Long> result = new ArrayList<>();
33 for (String string : strings) {
34 result.add(Long.parseLong(string));
35 }
36
37 return result;
38 }
39 /**
40 * 把逗号分隔字符串转换List的Long
41 *
42 * @param str
43 * @return
44 */
45 public static List<Long> splitStr2LongArr(String str,String split) {
46 String[] strings = splitStr2StrArr(str,split);
47 if (strings == null) return null;
48
49 List<Long> result = new ArrayList<>();
50 for (String string : strings) {
51 result.add(Long.parseLong(string));
52 }
53
54 return result;
55 }
56
57 public static String getRandomString(int length) {
58 String str = "0123456789";
59 Random random = new Random();
60 StringBuffer sb = new StringBuffer();
61 for (int i = 0; i < length; i++) {
62 int number = random.nextInt(10);
63 sb.append(str.charAt(number));
64 }
65 return sb.toString();
66
67 }
68
69 public static String getComplexRandomString(int length) {
70 String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
71 Random random = new Random();
72 StringBuffer sb = new StringBuffer();
73 for (int i = 0; i < length; i++) {
74 int number = random.nextInt(62);
75 sb.append(str.charAt(number));
76 }
77 return sb.toString();
78 }
79
80 public static String convertPropertiesToHtml(String properties){
81 //1:容量:6:32GB_4:样式:12:塑料壳
82 StringBuilder sBuilder = new StringBuilder();
83 String[] propArr = properties.split("_");
84 for (String props : propArr) {
85 String[] valueArr = props.split(":");
86 sBuilder.append(valueArr[1]).append(":").append(valueArr[3]).append("<br>");
87 }
88 return sBuilder.toString();
89 }
90
91}
92
93
MD5.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 1package org.leryoo.util.encrypt;
2
3/**
4 * 传入参数:一个字节数组
5 * 传出参数:字节数组的 MD5 结果字符串
6 */
7public class MD5 {
8 public static String getMD5(String sources) {
9 byte[] source = sources.getBytes();
10 String s = null;
11 char hexDigits[] = { // 用来将字节转换成 16 进制表示的字符
12 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
13 try {
14 java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
15 md.update(source);
16 byte tmp[] = md.digest(); // MD5 的计算结果是一个 128 位的长整数,
17 // 用字节表示就是 16 个字节
18 char str[] = new char[16 * 2]; // 每个字节用 16 进制表示的话,使用两个字符,
19 // 所以表示成 16 进制需要 32 个字符
20 int k = 0; // 表示转换结果中对应的字符位置
21 for (int i = 0; i < 16; i++) { // 从第一个字节开始,对 MD5 的每一个字节
22 // 转换成 16 进制字符的转换
23 byte byte0 = tmp[i]; // 取第 i 个字节
24 str[k++] = hexDigits[byte0 >>> 4 & 0xf]; // 取字节中高 4 位的数字转换,
25 // >>> 为逻辑右移,将符号位一起右移
26 str[k++] = hexDigits[byte0 & 0xf]; // 取字节中低 4 位的数字转换
27 }
28 s = new String(str); // 换后的结果转换为字符串
29 } catch (Exception e) {
30 e.printStackTrace();
31 }
32 return s;
33 }
34
35 public static boolean validateMD5(String key, String encryptSource) {
36 return encryptSource.equals(MD5.getMD5(key));
37 }
38
39 public static String getRandomCode(int length) {
40 String s = "";
41 for (int i = 0; i < length; i++)
42 s += (new java.util.Random()).nextInt(10);
43 return s;
44 }
45
46}
47
48
前端部分:
授权中心(用户中心):有统一的登录界面
登录实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 1login(){
2//4.发送ajax请求
3 this.$http.post("/user/sso/login",this.formParams).then(res=>{
4 var ajaxResult = res.data;
5 if(ajaxResult.success){
6 alert("登录成功");
7
8 let accessToken = ajaxResult.resultObj;
9 //通过cookie共享accessToken给其他站点 user不能直接放入cookie,因为不安全,但是可以
10 //存放access-token到时通过access-token就能获取用户了
11 setCookie("access-token",accessToken,"m30"); //session过期也是30分钟
12 //保存用户到localStorage
13 this.$http.get("/user/sso/ac/"+accessToken).then(res=>{
14 var user = res.data;
15 localStorage.setItem("user",user)
16 })
17 //跳转到主页面 localhost和127.0.0.1不同的不能共享cookie
18 //location.href = "http://user.hrm.com:6003/user.home.html"
19 location.href = "http://127.0.0.1:6003/user.home.html"
20 //以后所有对后端服务的访问都要携带accessToken
21 }else{
22 alert("登录失败:"+ajaxResult.message);
23 }
24 })
25}
26
27
Common.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107 1//JS操作cookies方法!
2
3//读取cookies
4function getCookie(name)
5{
6 var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
7 if(arr=document.cookie.match(reg)) return unescape(arr[2]);
8 else return null;
9}
10//删除cookies
11function delCookie(name)
12{
13 var exp = new Date();
14 exp.setTime(exp.getTime() - 1);
15 var cval=getCookie(name);
16 if(cval!=null) document.cookie= name + "="+cval+";expires="+exp.toGMTString();
17}
18//使用示例
19// setCookie("name","hayden");
20// alert(getCookie("name"));
21
22
23//如果需要设定自定义过期时间
24//那么把上面的setCookie 函数换成下面两个函数就ok;
25
26
27//写cookies
28function setCookie(name,value)
29{
30 var Days = 30;
31 var exp = new Date();
32 exp.setTime(exp.getTime() + Days*24*60*60*1000);
33 document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString();
34}
35//程序代码
36function setCookie(name,value,time){
37 var strsec = getsec(time);
38 var exp = new Date();
39 exp.setTime(exp.getTime() + strsec*1);
40 //所以hrm.com为父域名的任何路径都能共享cookie
41 //document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString()+";path=/"+";domain=.hrm.com";
42 document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString()+";path=/";
43}
44function getsec(str){
45 var str1=str.substring(1,str.length)*1;
46 var str2=str.substring(0,1);
47 if (str2=="s"){
48 return str1*1000;
49 }else if (str2=="h"){
50 return str1*60*60*1000;
51 }else if (str2=="m"){
52 return str1*60*1000;
53 }else if (str2=="d"){
54 return str1*24*60*60*1000;
55 }
56}
57//这是有设定过期时间的使用示例:
58//s20是代表20秒
59//h是指小时,如12小时则是:h12
60//d是天数,30天则:d30
61//暂时只写了这三种,不知道谁有更好的方法,呵呵
62// setCookie("name","hayden","s20");
63
64//axios初始化
65axios.interceptors.request.use(config => {
66 //如果已经登录了,每次都把token作为一个请求头传递过程
67
68 let accessToken = getCookie("access-token");
69 if (accessToken) {
70 // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
71 config.headers['access-token'] = accessToken;
72 }
73 console.debug('config',config)
74 return config
75}, error => {
76 // Do something with request error
77 Promise.reject(error)
78})
79axios.defaults.baseURL = "http://127.0.0.1:1030/services"//配置前缀
80Vue.prototype.$http = axios //给Vue这个类添加一个原型的属性,这个类的对象都能调用
81Vue.config.productionTip = false
82
83//登录拦截判断 时候有accessToken
84//是否能从localStrage获取获取用户,如果有自己跳过
85//否则需要获取用户,再跳过
86//var loginUrl = "http://user.hrm.com:6003/login.html"
87var loginUrl = "http://127.0.0.1:6003/login.html"
88$().ready(function(){
89 // 登录拦截 要放行 login.html register
90 let href = location.href;
91 if(href.indexOf("login")!=-1 || href.indexOf("reg") !=-1)
92 return;
93 let accessToken = getCookie("access-token");
94 if(!accessToken)
95 location.href = loginUrl;
96
97 let user = localStorage.getItem("user");
98 if(!user){
99 //保存用户到localStorage
100 axios.get("/user/sso/ac/"+accessToken).then(res=>{
101 var user = res.data;
102 localStorage.setItem("user",user)
103 })
104 }
105})
106
107
注意:
1 一定要保证同域名
2 引入js顺序 common.js是需要依赖axios