Springcloud微服务项目——人力资源管理(HRM)Day11 单点登录SSO

释放双眼,带上耳机,听听看~!

sso简介

百度百科

什么是单点登录(What)

单点登录( Single Sign-On , 简称 SSO )是目前比较流行的服务于企业登录业务整合的解决方案之一, SSO 使得在多个应用系统中,用户只需要 登录一次 就可以访问所有相互信任的应用系统。

比如:
Qq qq空间 qq游戏 qq邮箱
百度 百度百科 百度贴吧 百度网盘

为什么要使用sso(Why)

我们有多个前端站点,有多个站点是需要登录才能够访问的,不可能所有站点都要写一个登录,需要一个站点登录了其他站点就不需要登录了

方案设计(How)

方案1:依赖于一些权限框架 shiro security
方案2:用一个单点登录框架 cas
方案3:自己设计,直接写(知其然亦知其所以然)

原来的登录
Springcloud微服务项目——人力资源管理(HRM)Day11 单点登录SSO
微服务
Springcloud微服务项目——人力资源管理(HRM)Day11 单点登录SSO

代码实现(Just do it)

实现步骤:

  1. 后端服务保护处理-zuul拦截
  2. 登录

1 后端登录服务
2 前端登录实现

  1. 站点做登录检查

首先我们需要定义一个拦截器 将没有登录的请求全部拦截下来

位置
放到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界面进行一个放行

Springcloud微服务项目——人力资源管理(HRM)Day11 单点登录SSO

然后还有需要对注册界面的一个获取图片验证码,手机验证码的请求进行一个放行

然后是登录的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

Springcloud微服务项目——人力资源管理(HRM)Day11 单点登录SSO

给TA打赏
共{{data.count}}人
人已打赏
安全经验

如何避免Adsense违规封号

2021-10-11 16:36:11

安全经验

安全咨询服务

2022-1-12 14:11:49

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索