1.单点登录
1.1什么是单点登录
单点登录( Single Sign-On , 简称 SSO )是目前比较流行的服务于企业登录业务整合的解决方案之一, SSO 使得在多个应用系统中,用户只需要 登录一次 就可以访问所有相互信任的应用系统。
比如你注册了QQ,在QQ登陆过后,你可以不用再登陆直接访问QQ空间,QQ音乐等
1.2.为什么要使用sso单点登录
我们有多个前端站点,有多个站点是需要登录才能够访问的,不可能所有站点都要写一个登录,需要一个站点登录了其他站点就不需要登录了.
1.3.以往的登陆方式
2.sso的方案设计
方案1:依赖于一些权限框架 shiro security
方案2:用一个单点登录框架 cas
方案3:自己设计,直接写
这里我采用第三种方式手动实现一个sso
1)所有的前端站点如果访问时没有登录都要跳转到授权中心的统一登录界面
2)如果授权中心前端登录ok后,会通cookie里面accessToken的方式共享给其他站点
3)其他站点访问可以获取accessToken,通过它获取用户并且存储LocalStorage,并且后续的访问都要携带accessToken
所有站点都应该是同一父域,才支持token的共享
1)Localhost
2)127.0.0.1
4.代码实现:
后端代码:
网关:
package com.penny.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
@Component
public class LoginFilter extends ZuulFilter {
@Override
public String filterType() {
// 登录校验,肯定是在前置拦截
return "pre";
}
@Override
public int filterOrder() {
// 顺序设置为1
return 1;
}
//登录放行
@Override
public boolean shouldFilter() {
// 返回true,代表过滤器生效。
return true;
}
@Override
public Object run() throws ZuulException {
// 登录校验逻辑。
// 1)获取Zuul提供的请求上下文对象
RequestContext ctx = RequestContext.getCurrentContext();
// 2) 从上下文中获取request对象
HttpServletRequest req = ctx.getRequest();
//对登录放行
String requestURI = req.getRequestURI();
if (requestURI.contains("login"))
return null;
//对swagger放行
if (requestURI.contains("api-docs"))
return null;
//对注册验证码&短信验证码放行
if(requestURI.contains("verifycode")){
return null;
}
//对注册放行
if(requestURI.contains("register")){
return null;
}
// 3) 从请求中获取token
String token = req.getHeader("access-token");
// 4) 判断
if(token == null || "".equals(token.trim())){
// 没有token,登录校验失败,拦截
ctx.setSendZuulResponse(false);
// 返回401状态码。也可以考虑重定向到登录页。
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
// 校验通过,可以考虑把用户信息放入上下文,继续向后执行
return null;
}
}
单点登录Controller
package com.penny.controller;
import com.penny.service.ISsoService;
import com.penny.domain.Sso;
import com.penny.query.SsoQuery;
import com.penny.util.AjaxResult;
import com.penny.util.PageList;
import com.baomidou.mybatisplus.plugins.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/sso")
public class SsoController {
@Autowired
public ISsoService ssoService;
/**
* 保存和修改公用的
* @param sso 传递的实体
* @return Ajaxresult转换结果
*/
@PostMapping
public AjaxResult add(@RequestBody Sso sso){
try {
ssoService.insert(sso);
return AjaxResult.me();
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.me().setMessage("保存对象失败!"+e.getMessage());
}
}
/**
* 删除对象信息
* @param id
* @return
*/
@DeleteMapping(value="/{id}")
public AjaxResult delete(@PathVariable("id") Long id){
try {
ssoService.deleteById(id);
return AjaxResult.me();
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.me().setMessage("删除对象失败!"+e.getMessage());
}
}
/**
* 保存和修改公用的
* @param sso 传递的实体
* @return Ajaxresult转换结果
*/
@PutMapping
public AjaxResult addOrUpdate(@RequestBody Sso sso){
try {
if(sso.getId() != null){
ssoService.updateById(sso);
}else{
ssoService.insert(sso);
}
return AjaxResult.me();
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.me().setMessage("保存对象失败!"+e.getMessage());
}
}
//获取用户
@GetMapping("/{id}")
public Sso get(@PathVariable("id")Long id)
{
return ssoService.selectById(id);
}
/**
* 查看所有的员工信息
* @return
*/
@GetMapping
public List<Sso> list(){
return ssoService.selectList(null);
}
/**
* 分页查询数据
*
* @param query 查询对象
* @return PageList 分页对象
*/
@PostMapping("/list")
public PageList<Sso> json(@RequestBody SsoQuery query)
{
Page<Sso> page = new Page<Sso>(query.getPage(),query.getRows());
page = ssoService.selectPage(page);
return new PageList<Sso>(page.getTotal(),page.getRecords());
}
//注册
@PostMapping("/register")
public AjaxResult register(@RequestBody Map<String,String> params)
{
return ssoService.register(params);
}
//登录
@PostMapping("/login")
public AjaxResult login(@RequestBody Sso sso){
return ssoService.login(sso);
}
//传递token
@GetMapping("/ac/{accessToken}")
public Sso querySso(@PathVariable("accessToken") String accessToken){
System.out.println(accessToken);
return ssoService.querySso(accessToken);
}
}
SsoService
package com.penny.service;
import com.penny.domain.Sso;
import com.baomidou.mybatisplus.service.IService;
import com.penny.util.AjaxResult;
import java.util.Map;
/**
* <p>
* 会员登录账号 服务类
* </p>
*
* @author Penny
* @since 2020-02-28
*/
public interface ISsoService extends IService<Sso> {
/**
* 注册
* @param params
* @return
*/
AjaxResult register(Map<String,String> params);
/**
* 登陆
* @param sso
* @return
*/
AjaxResult login(Sso sso);
/**
* 传递token
* @param accessToken
* @return
*/
Sso querySso(String accessToken);
}
实现类:
package com.penny.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.penny.client.RedisClient;
import com.penny.domain.Sso;
import com.penny.domain.VipBase;
import com.penny.mapper.SsoMapper;
import com.penny.mapper.VipBaseMapper;
import com.penny.service.ISsoService;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.penny.util.AjaxResult;
import com.penny.util.StrUtils;
import com.penny.util.encrypt.MD5;
import org.aspectj.weaver.loadtime.Aj;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* <p>
* 会员登录账号 服务实现类
* </p>
*
* @author Penny
* @since 2020-02-28
*/
@Service
public class SsoServiceImpl extends ServiceImpl<SsoMapper, Sso> implements ISsoService {
@Autowired
private SsoMapper ssoMapper;
@Autowired
private VipBaseMapper vipBaseMapper;
@Autowired
private RedisClient redisClient;
@Override
public AjaxResult register(Map<String, String> params) {
String mobile = params.get("mobile");
String password = params.get("password");
String smsCode = params.get("smsCode");
//校验
AjaxResult result = validateParam(mobile,password,smsCode);
if(!result.isSuccess()){
return result;
}
//保存sso信息
Sso sso = new Sso();
sso.setPhone(mobile);
//获取随机验证字符串
String salt = StrUtils.getComplexRandomString(32);
//设置盐值
sso.setSalt(salt);
//使用随机验证给密码md5加密,并设置
//输入密码+以后做校验的时候先从数据库查询盐=md5,再和数据库查询出来进行比较
String Md5Password = MD5.getMD5(password + salt);
sso.setPassword(Md5Password);
sso.setNickName(mobile);
//设置安全等级
sso.setSecLevel(0);
sso.setBitState(1L);
sso.setCreateTime(System.currentTimeMillis());
ssoMapper.insert(sso);
//保存关联对象
VipBase vipBase = new VipBase();
vipBase.setCreateTime(System.currentTimeMillis());
vipBase.setSsoId(sso.getId());
vipBase.setRegChannel(1);
vipBase.setRegTime(System.currentTimeMillis());
vipBaseMapper.insert(vipBase);
return AjaxResult.me();
}
private AjaxResult validateParam(String mobile, String password, String smsCode) {
//手机号空验证
if(!StringUtils.hasLength(mobile) || !StringUtils.hasLength(password)){
return AjaxResult.me().setSuccess(false).setMessage("请输入正确的用户名或密码!");
}
//手机号已注册
//查询数据库
List<Sso> phone = ssoMapper.selectList(new EntityWrapper<Sso>().eq("phone", mobile));
if(phone != null && phone.size()>0){
return AjaxResult.me().setSuccess(false).setMessage("该手机号已注册");
}
// 短信验证码校验 通过key从redis获取
String smsCodeStr = redisClient.get("SMS_CODE:" + mobile);
String smsCodeRedis = smsCodeStr.split(":")[0];
if(!smsCodeRedis.equals(smsCode)){
return AjaxResult.me().setSuccess(false).setMessage("请输入正确的短信验证码");
}
return AjaxResult.me();
}
@Override
public AjaxResult login(Sso sso) {
//检查用户是否为空
if(!StringUtils.hasLength(sso.getPhone()) || !StringUtils.hasLength(sso.getPassword())){
return AjaxResult.me().setSuccess(false).setMessage("请输入正确的用户名或密码!");
}
//检查用户是否存在
List<Sso> phone = ssoMapper.selectList(new EntityWrapper<Sso>().eq("phone", sso.getPhone()));
if(phone==null || phone.size()<1){
return AjaxResult.me().setSuccess(false).setMessage("用户不存在,请注册!");
}
//从数据库查询sso
Sso ssoExsit = phone.get(0);
//进行密码比对-输入密码+数据库盐值=md5再和数据库密码比对
String MD5Pwd = MD5.getMD5(sso.getPassword() + ssoExsit.getSalt());
if(!ssoExsit.getPassword().equals(MD5Pwd)){
return AjaxResult.me().setSuccess(false).setMessage("请输入正确的用户名或密码!");
}
//用户存到redis并且返回token-60*30(单点登录)
String accessToken = UUID.randomUUID().toString();
redisClient.addForTime(accessToken, JSONObject.toJSONString(ssoExsit),30*60);
return AjaxResult.me().setResultObj(accessToken);
}
@Override
public Sso querySso(String accessToken) {
//登录时已经存放了redis,直接从redis获取就ok
String sso = redisClient.get(accessToken);
return JSONObject.parseObject(sso,Sso.class);
}
}
前端代码:
登陆模块:
<script type="text/javascript">
$().ready(function(){
new Vue({
el: ".login-wrap",
data() {
return {
formParams: {
phone: '15586136214',
password: '1',
}
}
},
methods: {
login(){
//4.发送ajax请求
this.$http.post("/user/sso/login",this.formParams).then(res=>{
var ajaxResult = res.data;
if(ajaxResult.success){
alert("登录成功");
let accessToken = ajaxResult.resultObj;
//通过cookie共享accessToken给其他站点 user不能直接放入cookie,因为不安全,但是可以
//存放access-token到时通过access-token就能获取用户了
setCookie("access-token",accessToken,"m30"); //session过期也是30分钟
//保存用户到localStorage
this.$http.get("/user/sso/ac/"+accessToken).then(res=>{
var user = res.data;
localStorage.setItem("user",user)
})
//跳转到主页面 localhost和127.0.0.1不同的不能共享cookie
//location.href = "http://user.hrm.com:6003/user.home.html"
location.href = "http://127.0.0.1:6003/user.home.html"
//以后所有对后端服务的访问都要携带accessToken
}else{
alert("登录失败:"+ajaxResult.message);
}
})
}
}
})
})
</script>
公共调用js
common.js:
/JS操作cookies方法!
//读取cookies
function getCookie(name)
{
var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
if(arr=document.cookie.match(reg)) return unescape(arr[2]);
else return null;
}
//删除cookies
function delCookie(name)
{
var exp = new Date();
exp.setTime(exp.getTime() - 1);
var cval=getCookie(name);
if(cval!=null) document.cookie= name + "="+cval+";expires="+exp.toGMTString();
}
//使用示例
// setCookie("name","hayden");
// alert(getCookie("name"));
//如果需要设定自定义过期时间
//那么把上面的setCookie 函数换成下面两个函数就ok;
//写cookies
function setCookie(name,value)
{
var Days = 30;
var exp = new Date();
exp.setTime(exp.getTime() + Days*24*60*60*1000);
document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString();
}
//程序代码
function setCookie(name,value,time){
var strsec = getsec(time);
var exp = new Date();
exp.setTime(exp.getTime() + strsec*1);
//所以hrm.com为父域名的任何路径都能共享cookie
//document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString()+";path=/"+";domain=.hrm.com";
document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString()+";path=/";
}
function getsec(str){
var str1=str.substring(1,str.length)*1;
var str2=str.substring(0,1);
if (str2=="s"){
return str1*1000;
}else if (str2=="h"){
return str1*60*60*1000;
}else if (str2=="m"){
return str1*60*1000;
}else if (str2=="d"){
return str1*24*60*60*1000;
}
}
//这是有设定过期时间的使用示例:
//s20是代表20秒
//h是指小时,如12小时则是:h12
//d是天数,30天则:d30
//暂时只写了这三种,不知道谁有更好的方法,呵呵
// setCookie("name","hayden","s20");
//axios初始化
axios.interceptors.request.use(config => {
//如果已经登录了,每次都把token作为一个请求头传递过程
let accessToken = getCookie("access-token");
if (accessToken) {
// 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
config.headers['access-token'] = accessToken;
}
console.debug('config',config)
return config
}, error => {
// Do something with request error
Promise.reject(error)
})
axios.defaults.baseURL = "http://127.0.0.1:1030/services"//配置前缀
Vue.prototype.$http = axios //给Vue这个类添加一个原型的属性,这个类的对象都能调用
Vue.config.productionTip = false
//登录拦截判断 时候有accessToken
//是否能从localStrage获取获取用户,如果有自己跳过
//否则需要获取用户,再跳过
//var loginUrl = "http://user.hrm.com:6003/login.html"
var loginUrl = "http://127.0.0.1:6003/login.html"
$().ready(function(){
// 登录拦截 要放行 login.html register
let href = location.href;
if(href.indexOf("login")!=-1 || href.indexOf("reg") !=-1)
return;
let accessToken = getCookie("access-token");
if(!accessToken)
location.href = loginUrl;
let user = localStorage.getItem("user");
if(!user){
//保存用户到localStorage
axios.get("/user/sso/ac/"+accessToken).then(res=>{
var user = res.data;
localStorage.setItem("user",user)
})
}
})
如其他网页也要做sso,则需要在那些网页也同时引入common.js做拦截检测
注意:
1 一定要保证同域名
2 引入js顺序 common.js是需要依赖axios