商城项目中实现单点登录功能后端部分

概要

SSO(single sign on)单点登录是一种身份验证机制,允许用户使用同一组凭据在多个应用程序或者系统中进行身份验证,而不需要对每一个用户程序用不同的凭据进行身份验证。
而实现SSO的一种常用方式就是使用令牌(token)的方式来管理用户的身份验证状态

系统架构

springboot+springcloud阿里巴巴+redis

基本实现步骤

1.用户从页面A跳转到登录页面后输入用户名密码进行登录
2.后台将接收到的用户名密码进行验证
3.验证通过后会用uuid生成token并且生成用户信息
4.以token为键将用户id和ip地址存入redis以便以后验证用户身份
5.将token和用户信息返回到前端
6.前端将token和用户信息保存到cookie中
7.重定向到用户进行登录的页面的之前的页面A
8.在页面A用cookie进行身份验证,通过后页面A显示用户信息
9.用户去访问页面B(页面B是必须需要登录才能访问的页面),首先进入网关的过滤器,①判断是否是微服务中调用内部接口(比如使用openfeign进行的接口调用),如果是,直接返回没有权限信息,这种接口属于服务器内部调用接口,不需要用户请求,如果不是继续进行下一步。②获取用户信息,通过页面传过来的cookie信息来拿到token,然后去查找以它为键去查找redis中是否有值,如果有值,从中取出ip地址和用户id,首先来判断用户的ip地址是否和存入redis时的ip地址相同,如果不相同返回错误信息,防止token被盗用,如果相同则进行下一步。③判断请求路径是否是需要登录的api异步接口,如果是,则根据第二步取出来的用户id判断是否为空,如果为空,返回未登录的错误信息。否则直接放行④判断请求路径是否是必须要登录的地址,如果是,则判断用户id是否为空,如果为空,则直接跳转到登录页面进行登录,否则就直接放行
10.由于cookie已经保存在浏览器中,因此页面B可以在过滤器的第四步中放行,放行后在页面B用浏览器中已存放的cookie进行身份验证,通过后在页面显示用户信息 访问其他页面同上

***注意点:一般来说单点登录在设置cookie的时候,可以将domain设置为父域名,而你需要访问的页面,都设置为父域名的子域名,例如gmall.com和list.gmall.com这样他们都能同时拥有你所设置的cookie
我由于一些原因没有设置域名而用本机回环地址代替

页面A虽然没有提及,但是需要注意的是访问每一个页面都需要先进入网关的过滤器,因为网关是统一访问路口,请求都是先进入网关, 再由网关进行映射

***

代码实现

1.用户点击登录之后后台进行用户验证 上面 1-5点

   /**
     * "title": "登录",
     * "path": "/api/user/passport/login",
     */
    @PostMapping("login")
    public Result login(@RequestBody UserInfo userInfo, HttpServletRequest request){
        //从后台数据库查询是否有此用户
       UserInfo userInfoPassport= userInfoService.login(userInfo);
       if (userInfoPassport!=null){
           Map<String,Object> map=new HashMap<>();
           //uuid生成token
           String token= UUID.randomUUID().toString().replaceAll("-","");
           //将用户信息和token存入map中返回
           map.put("nickName",userInfoPassport.getNickName());
           map.put("token",token);
           //将用户id和访问的ip地址存入redis中以便后续网关过滤器进行验证
           JSONObject jsonObject=new JSONObject();
           jsonObject.put("userId",userInfoPassport.getId().toString());
           jsonObject.put("IP", IpUtil.getIpAddress(request));
           redisTemplate.opsForValue().set(RedisConst.USER_LOGIN_KEY_PREFIX+token,jsonObject.toJSONString(),RedisConst.USERKEY_TIMEOUT, TimeUnit.SECONDS);
           return Result.ok(map);
       }
        return Result.fail().message("登录失败,用户名或密码错误");
    }

2.网关过滤器进行鉴权上面第9点

package com.atguigu.gmall.gateway.filter;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.IpUtil;
import com.netflix.client.ssl.URLSslContextFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

@Component
public class AuthGlobalFilter implements GlobalFilter {
    @Autowired
    private RedisTemplate redisTemplate;

    //匹配路径的工具类
    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Value("${authUrls.url}")
    private String authUrls;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取请求对象
        ServerHttpRequest request = exchange.getRequest();
        //获取URL
        String path = request.getURI().getPath();
        //让静态资源直接放行
        String staticResource="css,data,fonts,img,js,hm.js";
        String[] split1 = staticResource.split(",");
        for (String s : split1) {
            if (path.indexOf(s)!=-1){
                return chain.filter(exchange);
            }
        }

        //如果是内部接口
        if (antPathMatcher.match("/**/inner/**", path)) {
            //返回没有权限
            return out(exchange.getResponse(), ResultCodeEnum.PERMISSION);
        }
        //获取用户id
        String userId = this.getUserId(request);
        if (userId.equals("-1")){
            //如果返回-1表示请求的ip地址于之前redis中存的ip地址不一致,返回非法请求
            return out(exchange.getResponse(),ResultCodeEnum.ILLEGAL_REQUEST);
        }
        //如果是auth需要鉴权的接口
        if (antPathMatcher.match("/api/**/auth/**",path)){
            if (StringUtils.isEmpty(userId)){
                //如果用户id为空表示没有登录返回未登录
                return out(exchange.getResponse(),ResultCodeEnum.LOGIN_AUTH);
            }else{
                return chain.filter(exchange);
            }
        }
        //验证白名单,网关中的url
        String[] split = authUrls.split(",");
        if (split!=null){
            for (String s : split) {
                //当前请求地址包含登录的控制器域名并且用户id为空
                if (path.indexOf(s)!=-1&&StringUtils.isEmpty(userId)){
                    //如果用户id为空表示没有登录需要重定向到登录页面
                    //303状态码表示由于请求对应的资源存在着另一个URI,应使用重定向获取请求的资源
                    exchange.getResponse().setStatusCode(HttpStatus.SEE_OTHER);
                    System.out.println(request.getURI());
                    exchange.getResponse().getHeaders().set(HttpHeaders.LOCATION,"http://127.0.0.1:9001/login/login.html?originUrl="+request.getURI());
                    //重定向到登录
                    return exchange.getResponse().setComplete();
                }
            }
        }
        //将userId传给后端  方便后续操作
        if (!StringUtils.isEmpty(userId)){
            request.mutate().header("userId",userId).build();
            return chain.filter(exchange.mutate().request(request).build());
        }
        return chain.filter(exchange);

    }

    private String getUserId(ServerHttpRequest request) {
        //从头信息中查看是否有token 一般是异步请求
        HttpHeaders headers = request.getHeaders();
        String token = "";
        List<String> list = headers.get("token");
        if (!CollectionUtils.isEmpty(list)){
            token=  list.get(0);
        }
        //从cookie中查看是否有token 一般是同步请求
        HttpCookie httpCookie = request.getCookies().getFirst("token");
        if (httpCookie!=null){
            token = httpCookie.getValue();
        }
        if (!StringUtils.isEmpty(token)){
            //从redis中拿到token所对应的值
            Object object = redisTemplate.opsForValue().get("user:login:"+ token);
            String resultString="";
            if (object!=null){
                resultString = object.toString();
            }
            if (!StringUtils.isEmpty(resultString)){
                JSONObject jsonObject = JSONObject.parseObject(resultString);
                String userId = jsonObject.getString("userId");
                //获取登录时候的ip
                String ip = jsonObject.getString("IP");
                //获取当前请求的ip
                String curIP = IpUtil.getGatwayIpAddress(request);
                if (!curIP.equals(ip)){
                    return "-1";
                }
                return userId;
            }
        }
        return "";

    }

    /**
     * 制作鉴权失败返回信息
     *
     * @param response
     * @param permission
     * @return
     */
    private Mono<Void> out(ServerHttpResponse response, ResultCodeEnum permission) {
        Result<Object> build = Result.build(null, permission);
        byte[] bytes = JSONObject.toJSONString(build).getBytes(StandardCharsets.UTF_8);
        DataBuffer dataBuffer = response.bufferFactory().wrap(bytes);
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(dataBuffer));

    }
}

结果展示

用户第一次进入首页

在这里插入图片描述

利用cookie进行身份验证的前端代码:

//获取cookie中的用户信息
showInfo() {
                    // debugger
                    if(auth.getUserInfo()) {
                        this.userInfo = auth.getUserInfo()

                        console.log("--------"+this.userInfo.nickName)
                    }
                }
//js文件中获取cookie的具体方法       
getUserInfo() {
        if ($.cookie('userInfo')) {
            return JSON.parse($.cookie('userInfo'))
        }
        return null
    }     
//用户信息对象的定义
  data: {
               // keyword: [[${searchParam?.keyword}]],
                userInfo: {
                    nickName: '',
                    name: ''
                }
            }   
    
//用户信息的展示
<li  v-if="userInfo.nickName == ''" class="f-item"><span><a href="javascript:" @click="login()">登录</a></span> <span><a href="#">免费注册</a></span></li>
                        <li  v-if="userInfo.nickName != ''" class="f-item"><span>{{userInfo.nickName}}</span> <span><a href="javascript:" @click="logout()">退出</a></span></li>      
                

可以看到在第一次进入首页的时候,因为cookie中没有token和用户信息,所以不显示用户名,显示的是请登录

用户点击登录按钮

在这里插入图片描述

登录之后

在这里插入图片描述
在登录的时候,会将用户信息发给后台进行验证,验证通过,就会将生成的token和userinfo信息返回到前端,前端再将它保存到cookie里,之后再跳转到登录之前的页面
后端验证的代码已经展示,这里展示前端设置cookie和跳转页面的代码:

//登录的方法
 submitLogin() {
            	/*后台就可以使用UserInfo 对象接收*/
                login.login(this.user).then(response => {
                    //	debugger
                    if (response.data.code == 200) {
                        //  把token存在cookie中、也可以放在localStorage中
						//	result.ok(map);
						//	response 相当于Result
						//	response.data 相当于 Result.ok();
						//	response.data.data  data看做一个map 相当于 Result.ok(data);
						//	response.data.map.token 相当于 result.ok(map)
						//	map 中有token 同时还有 nickName
                        auth.setToken(response.data.data.token)
						//	response.data.data 相当于返回来的map
						//	JSON.stringify(response.data.data) 将map 数据变为json对象,存储到cookie 中
                        auth.setUserInfo(JSON.stringify(response.data.data))



						//	输入日志:是否有 originUrl 回跳url!
						//	originUrl = 记录用户在哪点击的登录url ,当用户登录成功之后,又跳转到了原来的url!
                        console.log("originUrl:"+this.originUrl);
                        if(this.originUrl == ''){
                            window.location.href="http://127.0.0.1:9001/index"
                            return ;
                        } else {
                            //重定向到页面的登录前页面
                            window.location.href = decodeURIComponent(this.originUrl)
						}
                    } else {
						alert(response.data.data.message)
					}

                })
            }
 //设置cookie的具体代码
 setToken(token) {
        return $.cookie('token', token, { expires: 7, path: '/'})
    },
  setUserInfo(userInfo) {
        return $.cookie('userInfo', userInfo, { expires: 7, path: '/'})
    }

再访问另一个需要登录的页面

在这里插入图片描述

从上面的代码可以看出我的cookie已经设置过了,path为/表示根路径,由于在设置cookie的时候没有设置domain,所以cookie可以有效的域名默认是设置cookie的页面,由于设置cookie的页面是登录页面,而登录页面的ip地址是127.0.0.1,因此只要ip地址是127.0.0.1的页面都拥有这样的cookie,因此可以正常显示用户名

如果退出登录,再次访问需要登录的页面(还是上文的list页面)

在这里插入图片描述

由于退出登录以后,浏览器中cookie的token和userinfo都置为了空,redis中存储的这个用户的信息也被删除,而在请求这个页面的时候,首先进入的是网关的过滤器,则会执行这些代码

 //当前请求地址包含登录的控制器域名并且用户id为空
                if (path.indexOf(s)!=-1&&StringUtils.isEmpty(userId)){
                    //如果用户id为空表示没有登录需要重定向到登录页面
                    //303状态码表示由于请求对应的资源存在着另一个URI,应使用重定向获取请求的资源
                    exchange.getResponse().setStatusCode(HttpStatus.SEE_OTHER);
                    System.out.println(request.getURI());
                    exchange.getResponse().getHeaders().set(HttpHeaders.LOCATION,"http://127.0.0.1:9001/login/login.html?originUrl="+request.getURI());
                    //重定向到登录
                    return exchange.getResponse().setComplete();
                }

由于token值已经为空,因此无法取出用户id,因此就会被重定向到登录页面进行登录

如果登录成功,后端又返回了token和userinfo数据,前端也设置了cookie,之后就能跳转到登录之前所访问的页面,正常的显示出用户信息

在list页面登录成功后

在这里插入图片描述

小问题

为什么用户在登录之后会跳转到登录之前的页面呢?
这是由于在跳转到登录页面的时候,会添加一个以originUrl为键以请求源地址为值的参数,而在登录成功的时候会判断originUrl是否为空,如果为空,跳转到首页,如果不为空,就跳转到originUrl所携带的地址
代码展示:

 login() {
 //前端跳转到登录页面时
                    //window.location.href:页面的跳转
                    //window.location.href:获取地址栏的内容
                    /*路径一样不要设置错我设置成了127.0.1.1*/
                    window.location.href = 'http://127.0.0.1:9001/login/login.html?originUrl='+window.location.href
                }

//后端在过滤器中,哪些需要登录的页面验证token失败,跳转到登录页面的代码
  if (path.indexOf(s)!=-1&&StringUtils.isEmpty(userId)){
                    //如果用户id为空表示没有登录需要重定向到登录页面
                    //303状态码表示由于请求对应的资源存在着另一个URI,应使用重定向获取请求的资源
                    exchange.getResponse().setStatusCode(HttpStatus.SEE_OTHER);
                    System.out.println(request.getURI());
                    exchange.getResponse().getHeaders().set(HttpHeaders.LOCATION,"http://127.0.0.1:9001/login/login.html?originUrl="+request.getURI());
                    //重定向到登录
                    return exchange.getResponse().setComplete();
                }


//前端在登录成功并且设置完Cookie信息后跳转的代码
 if(this.originUrl == ''){
                            window.location.href="http://127.0.0.1:9001/index"
                            return ;
                        } else {
                            //重定向到页面的登录前页面
                            window.location.href = decodeURIComponent(this.originUrl)
						}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值