SpringBoot SSO轻松实现(附demo)
作者:何裕华 发布时间:2022-04-05 02:24:33
标签:SpringBoot,SSO
前言
网上SSO的框架很多,此篇文章使用的是自写的SSO来实现简单的登录授权功能,目的在于扩展性,权限这方面,自写扩展性会好点。
提示:以下是本篇文章正文内容,下面案例可供参考
一、技术介绍
1.SSO是什么?
单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的。
二、使用步骤
1.引入maven库
代码如下(示例):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
<relativePath/>
</parent>
<dependencies>
<dependencies>
<dependency>
<artifactId>hyh-boot-starter-redis</artifactId>
<groupId>com.hyh.redis</groupId>
<version>1.0.0</version>
</dependency>
</dependencies>
2.具体使用示例
ILogin接口:
package com.hyh.sso;
import com.hyh.sso.po.LoginResult;
/**
* 登录接口
*
* @Author: heyuhua
* @Date: 2021/1/8 17:14
*/
public interface ILogin {
/**
* 登录
*
* @param account 用户名
* @param password 密码
* @param callbackUrl 用户验证回调URL
* @return
*/
LoginResult login(String account, String password, String callbackUrl);
}
登录状态枚举:
package com.hyh.sso;
/**
* 登录状态枚举
*
* @Author: heyuhua
* @Date: 2021/1/8 16:59
*/
public enum LoginStatus {
SUCCESS(1, "登录成功"), ING(0, "登录中"), FAIL(-1, "登录失败"),
ERROR(-2, "登录异常"), CALLBACK_ERROR(-3, "登录回调异常"), ACCOUNT_LOCK(-4, "账户被锁定"),
EXPIRE(-5,"登录用户已过期");
/**
* 登录状态码
*/
private int code;
/**
* 登录状态消息
*/
private String message;
private LoginStatus(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
登录类型枚举:
package com.hyh.sso;
/**
* 登录类型
*
* @Author: heyuhua
* @Date: 2021/1/8 17:16
*/
public enum LoginTypes {
/**
* 登入
*/
IN,
/**
* 登出
*/
OUT;
}
登录常规接口:
package com.hyh.sso;
package com.hyh.sso.service;
import com.hyh.sso.ILogin;
/**
* 常规登录接口
*
* @Author: heyuhua
* @Date: 2021/1/8 17:54
*/
public interface LoginService extends ILogin {
}
登录接口实现:
package com.hyh.sso.service.impl;
import com.alibaba.fastjson.JSON;
import com.hyh.sso.LoginStatus;
import com.hyh.sso.po.LoginResult;
import com.hyh.sso.po.LoginUser;
import com.hyh.sso.service.LoginService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* 登录接口实现
*
* @Author: heyuhua
* @Date: 2021/1/8 17:56
*/
@Service
public class LoginServiceImpl implements LoginService {
private static final Logger LOG = LoggerFactory.getLogger(LoginServiceImpl.class);
/**
* rest接口请求模板
*/
private static RestTemplate restTemplate = new RestTemplate();
@Override
public LoginResult login(String account, String password, String callbackUrl) {
LoginResult loginResult = null;
try {
HttpHeaders headers = new HttpHeaders();
//设置请求媒体数据类型
headers.setContentType(MediaType.APPLICATION_JSON);
//设置返回媒体数据类型
headers.add("Accept", MediaType.APPLICATION_JSON.toString());
HttpEntity<String> formEntity = new HttpEntity<String>(JSON.toJSONString(new LoginUser(account, password)), headers);
loginResult = restTemplate.postForObject(callbackUrl, formEntity, LoginResult.class);
} catch (Exception e) {
LOG.error("login valid callback error", e);
return new LoginResult(LoginStatus.CALLBACK_ERROR);
}
return loginResult == null ? new LoginResult(LoginStatus.ERROR) : loginResult;
}
}
登录用户对象:
package com.hyh.sso.po;
/**
* 登录用户对象
*
* @Author: heyuhua
* @Date: 2021/1/8 16:58
*/
public class LoginUser {
/**
* 账号
*/
private String account;
/**
* 密码
*/
private String password;
/**
* 登录时间
*/
private String loginTime;
public LoginUser(String account, String password) {
this.account = account;
this.password = password;
}
public LoginUser() {
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getLoginTime() {
return loginTime;
}
public void setLoginTime(String loginTime) {
this.loginTime = loginTime;
}
}
用户Token对象:
package com.hyh.sso.po;
import com.hyh.utils.code.MD5;
import com.hyh.utils.common.StringUtils;
import java.util.Calendar;
/**
* 用户Token对象
*
* @Author: heyuhua
* @Date: 2021/1/8 17:07
*/
public class UserToken {
/**
* token
*/
private String token;
/**
* 过期时间
*/
private String expireTime;
public UserToken(String token, String expireTime) {
this.token = token;
this.expireTime = expireTime;
}
public UserToken() {
}
public static UserToken getUserToken() {
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.MINUTE, 30);
return new UserToken(MD5.getMD5String(StringUtils.ranStr(32)), String.valueOf(nowTime.getTimeInMillis()));
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getExpireTime() {
return expireTime;
}
public void setExpireTime(String expireTime) {
this.expireTime = expireTime;
}
/**
* 生成Token
*/
private String generateToken() {
return MD5.getMD5String(StringUtils.ranStr(32));
}
}
登录结果对象:
package com.hyh.sso.po;
import com.hyh.sso.LoginStatus;
import com.hyh.sso.LoginTypes;
/**
* 登录结果对象
* @Author: heyuhua
* @Date: 2021/1/8 16:58
*/
public class LoginResult {
/**
* 登录用户对象
*/
private LoginUser loginUser;
/**
* 登录用户令牌
*/
private UserToken userToken;
/**
* 登录状态
*/
private LoginStatus loginStatus;
/**
* 登录类型
*/
private LoginTypes loginTypes;
public LoginResult(){}
public LoginResult(LoginStatus loginStatus) {
this.loginStatus = loginStatus;
}
public LoginUser getLoginUser() {
return loginUser;
}
public void setLoginUser(LoginUser loginUser) {
this.loginUser = loginUser;
}
public UserToken getUserToken() {
return userToken;
}
public void setUserToken(UserToken userToken) {
this.userToken = userToken;
}
public LoginStatus getLoginStatus() {
return loginStatus;
}
public void setLoginStatus(LoginStatus loginStatus) {
this.loginStatus = loginStatus;
}
public LoginTypes getLoginTypes() {
return loginTypes;
}
public void setLoginTypes(LoginTypes loginTypes) {
this.loginTypes = loginTypes;
}
@Override
public String toString() {
return "LoginResult{" +
"loginUser=" + loginUser +
", userToken=" + userToken +
", loginStatus=" + loginStatus +
", loginTypes=" + loginTypes +
'}';
}
}
登录助手:
package com.hyh.sso.helper;
import com.alibaba.fastjson.JSON;
import com.hyh.redis.helper.RedisHelper;
import com.hyh.sso.LoginStatus;
import com.hyh.sso.po.LoginResult;
import com.hyh.sso.po.LoginUser;
import com.hyh.sso.po.UserToken;
import com.hyh.sso.service.LoginService;
import com.hyh.utils.common.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import javax.annotation.Resource;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* 登录助手
*
* @Author: heyuhua
* @Date: 2021/1/8 17:13
*/
@Component
public class LoginHelper {
/**
* 日志
*/
private static final Logger LOG = LoggerFactory.getLogger(LoginHelper.class);
/**
* 登录用户信息KEY
*/
private final String LOGIN_USER_KEY = "login:user:";
/**
* 登录用户TOKEN KEY
*/
private final String LOGIN_TOKEN_KEY = "login:token:";
/**
* 登录失败统计 KEY
*/
private final String LOGIN_FAIL_COUNT_KEY = "login:fail:count";
/**
* 登录失败最多允许次数
*/
private final long MAX_FAIL_COUNT = 5;
/**
* 登录服务
*/
@Resource
private LoginService loginService;
/**
* redis助手
*/
@Autowired
private RedisHelper redisHelper;
/**
* 登录
*
* @param account 用户名
* @param password 密码
* @param callbackUrl 回调URL
* @return
*/
public LoginResult login(String account, String password, String callbackUrl) {
Assert.notNull(account, "account is null ");
Assert.notNull(password, "password is null ");
Assert.notNull(callbackUrl, "callbackUrl is null ");
//判断账户是否多次登录失败被锁定
String value = redisHelper.getStringValue(LOGIN_FAIL_COUNT_KEY + account);
if (StringUtils.isNotBlank(value)) {
Long loginFailCount = Long.parseLong(value);
if (loginFailCount.longValue() >= MAX_FAIL_COUNT) {
return new LoginResult(LoginStatus.ACCOUNT_LOCK);
}
}
//登录操作
LoginResult loginResult = loginService.login(account, password, callbackUrl);
switch (loginResult.getLoginStatus()) {
case SUCCESS:
//登录成功
loginSuccess(loginResult);
break;
case FAIL:
//登录失败
loginFail(loginResult);
break;
case ERROR:
loginError(loginResult);
//登录异常
break;
default:
break;
}
return loginResult;
}
/**
* 注销
*
* @param account
* @param token
*/
public void logout(String account, String token) {
Assert.notNull(account, "account is null ");
Assert.notNull(token, "token is null ");
removeKey(account, token);
}
/**
* 注销
*
* @param token
*/
public void logout(String token) {
Assert.notNull(token, "token is null ");
removeKey(token);
}
/**
* 获取登录用户
*
* @param token
* @return
*/
public LoginUser getLoginUser(String token) {
Assert.notNull(token, "token is null ");
String value = redisHelper.getStringValue(LOGIN_USER_KEY + token);
if (StringUtils.isNotBlank(value)) {
return JSON.parseObject(value, LoginUser.class);
}
return null;
}
/**
* 移除 key
*
* @param account
* @param token
*/
private void removeKey(String account, String token) {
redisHelper.del(LOGIN_FAIL_COUNT_KEY + account);
redisHelper.del(LOGIN_TOKEN_KEY + account);
redisHelper.del(LOGIN_USER_KEY + token);
}
/**
* 移除 Key
*
* @param token
*/
private void removeKey(String token) {
redisHelper.del(LOGIN_USER_KEY + token);
//其余的key到达过期时间自动过期
}
/**
* 登录异常
*
* @param loginResult
*/
private void loginError(LoginResult loginResult) {
LOG.error("user 【" + loginResult.getLoginUser().getAccount() + "】 login error");
}
/**
* 登录失败操作
*
* @param loginResult
*/
private void loginFail(LoginResult loginResult) {
String key = LOGIN_FAIL_COUNT_KEY + loginResult.getLoginUser();
redisHelper.increment(key, 30 * 60 * 1000);
}
/**
* 登录成功操作
*
* @param loginResult
*/
private void loginSuccess(LoginResult loginResult) {
LoginUser loginUser = loginResult.getLoginUser();
loginUser.setLoginTime(String.valueOf(new Date().getTime()));
UserToken userToken = UserToken.getUserToken();
redisHelper.set(LOGIN_TOKEN_KEY + loginResult.getLoginUser().getAccount(), JSON.toJSONString(userToken), 30, TimeUnit.MINUTES);
redisHelper.set(LOGIN_USER_KEY + userToken.getToken(), JSON.toJSONString(loginUser), 30, TimeUnit.MINUTES);
redisHelper.del(LOGIN_FAIL_COUNT_KEY + loginResult.getLoginUser());
}
}
3.配置文件
代码如下(示例):
server:
port: 8088
spring:
#redis配置
redis:
host: 192.168.6.134
port: 30511
password:
4.单元测试
测试代码如下(示例):
@Autowired
private LoginHelper loginHelper;
@Test
public void testLogin() {
//测试时先开启HyhBootApplication
String account = "hyh";
String password = "hyh-pwd";
String cllbackUrl = "http://localhost:8088/hyh/login";//在com.hyh.core.web下可查看
LoginResult loginResult = loginHelper.login(account, password, cllbackUrl);
System.out.println("loginResult:" + loginResult.toString());
}
//控制层代码
@RequestMapping(value = "login", method = RequestMethod.POST)
public LoginResult login(@RequestBody LoginUser loginUser) {
Assert.notNull(loginUser.getAccount(), "account is null");
Assert.notNull(loginUser.getPassword(), "password is null");
LoginResult loginResult = new LoginResult(LoginStatus.SUCCESS);
loginResult.setLoginUser(loginUser);
//模拟直接返回登录成功
return loginResult;
}
总结
是不是感觉很简单?更多用法请点击下方查看源码,关注我带你揭秘更多高级用法
源码地址:点此查看源码.
来源:https://juejin.cn/post/6922620512107823112


猜你喜欢
- 本文为个人理解,不保证完全正确。官方文档中将双冒号的用法分为4类,按照我的个人理解可以分成2类来使用。官方文档官方文档中将双冒号的用法分为了
- 服务限流,是指通过控制请求的速率或次数来达到保护服务的目的,在微服务中,我们通常会将它和熔断、降级搭配在一起使用,来避免瞬时的大量请求对系统
- 今天从数据库生成了一份数据字典,但是没有备注,所以需要程序把表格都读出来。用到了下面的代码,亲测可用~~object oFileName =
- 本文实例讲述了java版微信公众平台消息接口应用方法。分享给大家供大家参考,具体如下:微信公众平台现在推出自动回复消息接口,但是由于是接口内
- 编写程序,利用continue语句实现循环体过滤器,过滤“老鹰”字符串,并做相应的处理,但是放弃continue语句之后的所有代码。即若遇到
- C#事件标准命名规则一些开源代码的事件命名很混乱,以此文章用作本人以后工作的参考。事件的名称事件始终是指某个操作,这个操作可能正在发生,也可
- 本文实例为大家分享了WPF实现平面三角形3D运动效果的具体代码,供大家参考,具体内容如下实现效果如下:思路:封装三角形三个顶点和路径的三角形
- 一、背景假如:黑客黑进了数据库,或者离职人员导出了数据,那么就可能导致这些敏感数据的泄漏。因此我们就需要找到一种方法来解决这个问题。二、解决
- 使用WebClient和htmlunit实现简易爬虫import com.gargoylesoftware.htmlunit.WebClie
- Android 自定义按钮点击事件和长按事件对比一个按钮同时实现点击和长按事件,有时候会有冲突,我们针对这一现象来自定义按钮来区
- spring boot metrics是什么?针对应用监控指标暴露,spring boot有一套完整的解决方案,并且内置了好很多的指标收集器
- 前言在产品发布前夕,经常因为编写各类设计文档感到心碎,倒不是难,而是比较繁琐,举例来说,像编写数据库文档这种操作来说,对于新手,甚至很多有一
- 一、概述System.Net.WebClient属于高层类、使用简单。均支持异步版本。支持http,https,fpt,files等URI。
- 本文实例讲述了android动态布局之动态加入TextView和ListView的方法。分享给大家供大家参考。具体实现方法如下:packag
- 前言当您使用LINQ来处理数据库时,这种体验是一种神奇的体验,对吗?你把数据库实体像一个普通的收集,使用Linq中像Where,Select
- 1.概述在实际开发过程中,我们经常需要调用对方提供的接口或测试自己写的接口是否合适。很多项目都会封装规定好本身项目的接口规范,所以大多数需要
- 前言内存泄漏简单地说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系
- 业务需要动态给图片增加文字(书本的封面图片),修改字体大小、字体、颜色、控制位置测试代码:string path = @"E:\c
- 详解 Corba开发之Java实现Service与Client1 概述
- 使用@Indexed加快启动速度Spring读取@Component组件(派生性),有两种实现方式,一种是反射,一种是ASM。反射性能低主要