基于SpringBoot实现单点登录系统
共 10032字,需浏览 21分钟
·
2020-12-31 18:46
来源 | urlify.cn/I3eyAz
单点登录系统设计思路:采用Spring4 Java配置方式整合HttpClient,Redis ,MySql和SpringBoot的简易教程。
在传统的系统,或者是只有一个服务器的系统中。Session在一个服务器中,各个模块都可以直接获取,只需登录一次就进入各个模块。若在服务器集群或者是分布式系统架构中,每个服务器之间的Session并不是共享的,这会出现每个模块都要登录的情况。这时候需要通过单点登录系统(Single Sign On)将用户信息存在Redis数据库中实现Session共享的效果。从而实现一次登录就可以访问所有相互信任的应用系统。
一、整合 HttpClient
HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。
首先在src/main/resources 目录下创建 httpclient.properties 配置文件。
#设置整个连接池默认最大连接数
http.defaultMaxPerRoute=100
#设置整个连接池最大连接数
http.maxTotal=300
#设置请求超时
http.connectTimeout=1000
#设置从连接池中获取到连接的最长时间
http.connectionRequestTimeout=500
#设置数据传输的最长时间
http.socketTimeout=10000
然后在 src/main/java/com/itdragon/config 目录下创建 HttpclientSpringConfig.java 文件
这里用到了四个很重要的注解
@Configuration : 作用于类上,指明该类就相当于一个xml配置文件
@Bean : 作用于方法上,指明该方法相当于xml配置中的bean,注意方法名的命名规范
@PropertySource : 指定读取的配置文件,引入多个value={“xxx:xxx”,“xxx:xxx”},ignoreResourceNotFound=true 文件不存在时忽略
@Value : 获取配置文件的值
package com.itdragon.config;
/**
* @Configuration 作用于类上,相当于一个xml配置文件
* @Bean 作用于方法上,相当于xml配置中的
* @PropertySource 指定读取的配置文件,ignoreResourceNotFound=true 文件不存在是忽略
* @Value 获取配置文件的值
*/
@Configuration
@PropertySource(value = "classpath:httpclient.properties", ignoreResourceNotFound=true)
public class HttpclientSpringConfig {
@Value("${http.maxTotal}")
private Integer httpMaxTotal;
@Value("${http.defaultMaxPerRoute}")
private Integer httpDefaultMaxPerRoute;
@Value("${http.connectTimeout}")
private Integer httpConnectTimeout;
@Value("${http.connectionRequestTimeout}")
private Integer httpConnectionRequestTimeout;
@Value("${http.socketTimeout}")
private Integer httpSocketTimeout;
@Autowired
private PoolingHttpClientConnectionManager manager;
@Bean
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
// 最大连接数
poolingHttpClientConnectionManager.setMaxTotal(httpMaxTotal);
// 每个主机的最大并发数
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(httpDefaultMaxPerRoute);
return poolingHttpClientConnectionManager;
}
@Bean // 定期清理无效连接
public IdleConnectionEvictor idleConnectionEvictor() {
return new IdleConnectionEvictor(manager, 1L, TimeUnit.HOURS);
}
@Bean // 定义HttpClient对象 注意该对象需要设置scope="prototype":多例对象
@Scope("prototype")
public CloseableHttpClient closeableHttpClient() {
return HttpClients.custom().setConnectionManager(this.manager).build();
}
@Bean // 请求配置
public RequestConfig requestConfig() {
return RequestConfig.custom().setConnectTimeout(httpConnectTimeout) // 创建连接的最长时间
.setConnectionRequestTimeout(httpConnectionRequestTimeout) // 从连接池中获取到连接的最长时间
.setSocketTimeout(httpSocketTimeout) // 数据传输的最长时间
.build();
}
}
二、整合 Redis
SpringBoot官方其实提供了spring-boot-starter-redis pom 帮助我们快速开发,但我们也可以自定义配置,这样可以更方便地掌控。
首先在src/main/resources 目录下创建 redis.properties 配置文件
redis.maxTotal=200
redis.node.host=10.128.15.21
redis.node.port=6379
REDIS_USER_SESSION_KEY=REDIS_USER_SESSION
SSO_SESSION_EXPIRE=30
设置Redis主机的ip地址和端口号,和存入Redis数据库中的key以及存活时间。这里为了方便测试,存活时间设置的比较小。这里的配置是单例Redis。
在src/main/java/com/itdragon/config 目录下创建 RedisSpringConfig.java 文件。
@Configuration
@PropertySource(value = "classpath:redis.properties")
public class RedisSpringConfig {
@Value("${redis.maxTotal}")
private Integer redisMaxTotal;
@Value("${redis.node.host}")
private String redisNodeHost;
@Value("${redis.node.port}")
private Integer redisNodePort;
private JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(redisMaxTotal);
return jedisPoolConfig;
}
@Bean
public JedisPool getJedisPool(){ // 省略第一个参数则是采用 Protocol.DEFAULT_DATABASE
JedisPool jedisPool = new JedisPool(jedisPoolConfig(), redisNodeHost, redisNodePort);
return jedisPool;
}
@Bean
public ShardedJedisPool shardedJedisPool() {
List jedisShardInfos = new ArrayList();
jedisShardInfos.add(new JedisShardInfo(redisNodeHost, redisNodePort));
return new ShardedJedisPool(jedisPoolConfig(), jedisShardInfos);
}
}
三、Service 层
在src/main/java/com/itdragon/service 目录下创建 UserService.java 文件,它负责三件事情
第一件事情:验证用户信息是否正确,并将登录成功的用户信息保存到Redis数据库中。
第二件事情:负责判断用户令牌是否过期,若没有则刷新令牌存活时间。
第三件事情:负责从Redis数据库中删除用户信息。
package com.itdragon.service;
@Service
@Transactional
@PropertySource(value = "classpath:redis.properties")
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private JedisClient jedisClient;
@Value("${REDIS_USER_SESSION_KEY}")
private String REDIS_USER_SESSION_KEY;
@Value("${SSO_SESSION_EXPIRE}")
private Integer SSO_SESSION_EXPIRE;
public Result registerUser(User user) {
// 检查用户名是否注册,一般在前端验证的时候处理,因为注册不存在高并发的情况,这里再加一层查询是不影响性能的
if (null != userRepository.findByAccount(user.getAccount())) {
return Result.build(400, "");
}
userRepository.save(user);
// 注册成功后选择发送邮件激活。现在一般都是短信验证码
return Result.build(200, "");
}
public Result userLogin(String account, String password,
HttpServletRequest request, HttpServletResponse response) {
// 判断账号密码是否正确
User user = userRepository.findByAccount(account);
if(user == null){
return Result.build(400, "账号名或密码错误");
}
if (!CheckUtils.decryptPassword(user, password)) {
return Result.build(400, "账号名或密码错误");
}
// 生成token
String token = UUID.randomUUID().toString();
// 清空密码和盐避免泄漏
String userPassword = user.getPassword();
String userSalt = user.getSalt();
user.setPassword(null);
user.setSalt(null);
// 把用户信息写入 redis
jedisClient.set(REDIS_USER_SESSION_KEY + ":" + token, JsonUtils.objectToJson(user));
// user 已经是持久化对象了,被保存在了session缓存当中,若user又重新修改了属性值,那么在提交事务时,此时 hibernate对象就会拿当前这个user对象和保存在session缓存中的user对象进行比较,如果两个对象相同,则不会发送update语句,否则,如果两个对象不同,则会发出update语句。
user.setPassword(userPassword);
user.setSalt(userSalt);
// 设置 session 的过期时间
jedisClient.expire(REDIS_USER_SESSION_KEY + ":" + token, SSO_SESSION_EXPIRE);
// 添加写 cookie 的逻辑,cookie 的有效期是关闭浏览器就失效。
CookieUtils.setCookie(request, response, "USER_TOKEN", token);
// 返回token
return Result.ok(token);
}
public void logout(String token) {
jedisClient.del(REDIS_USER_SESSION_KEY + ":" + token);
}
public Result queryUserByToken(String token) {
// 根据token从redis中查询用户信息
String json = jedisClient.get(REDIS_USER_SESSION_KEY + ":" + token);
// 判断是否为空
if (StringUtils.isEmpty(json)) {
return Result.build(400, "此session已经过期,请重新登录");
}
// 更新过期时间
jedisClient.expire(REDIS_USER_SESSION_KEY + ":" + token, SSO_SESSION_EXPIRE);
// 返回用户信息
return Result.ok(JsonUtils.jsonToPojo(json, User.class));
}
}
四、Controller 层
负责跳转登录页面跳转,负责用户的登录,退出,获取令牌的操作。UserController.java和PageController.java
package com.itdragon.controller;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value="/login", method=RequestMethod.POST)
@ResponseBody
public Result userLogin(String username, String password,
HttpServletRequest request, HttpServletResponse response) {
try {
Result result = userService.userLogin(username, password, request, response);
return result;
} catch (Exception e) {
e.printStackTrace();
return Result.build(500, "");
}
}
@RequestMapping(value="/logout/{token}")
public String logout(@PathVariable String token) {
userService.logout(token); // 思路是从Redis中删除key,实际情况请和业务逻辑结合
return "back";
}
@RequestMapping("/token/{token}")
@ResponseBody
public Object getUserByToken(@PathVariable String token) {
Result result = null;
try {
result = userService.queryUserByToken(token);
} catch (Exception e) {
e.printStackTrace();
result = Result.build(500, "");
}
return result;
}
}
package com.itdragon.controller;
@Controller
public class PageController {
@RequestMapping("/login")
public String showLogin(String redirect, Model model) {
model.addAttribute("redirect", redirect);
return "login";
}
}
五、视图层
一个简单的登录页面和资源展示页面。login.jsp、index.jsp和indexHomePage.jsp
六、Spring 自定义拦截器
这里是另外一个项目 service-test-sso 中的代码,首先在src/main/resources/spring/springmvc.xml 中配置拦截器,设置哪些请求需要拦截
"com.it.controller" />
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
"prefix" value="/WEB-INF/views/" />
"suffix" value=".jsp" />
"/WEB-INF/static/" mapping="/static/**"/>
"/indexHomePage/**"/>
"com.it.interceptors.UserLoginHandlerInterceptor"/>
UserLoginHandlerInterceptor.java
package com.it.interceptors;
public class UserLoginHandlerInterceptor implements HandlerInterceptor {
public static final String COOKIE_NAME = "USER_TOKEN";
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String token = CookieUtils.getCookieValue(request, COOKIE_NAME);
User user = this.userService.getUserByToken(token);
if (StringUtils.isEmpty(token) || null == user) {
// 跳转到登录页面,把用户请求的url作为参数传递给登录页面。
response.sendRedirect("http://localhost:8081/login?redirect=" + request.getRequestURL());
// 返回false
return false;
}
// 把用户信息放入Request
request.setAttribute("user", user);
// 返回值决定handler是否执行。true:执行,false:不执行。
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
}
}
七、操作步骤
测试思路:
第一步:注册用户,执行sso 项目下SpringbootStudyApplicationTests.java 单元测试类中的 registerUser() 方法添加用户。
第二步:开启sso服务。
第三步:再开启两个service-test-sso服务。
第四步:在service-test-sso服务页面点击“访问主页”按钮进入权限页面测试。
八、sso项目结构
service-test-sso项目结构
访问主页
点击登录
用户表存储如下
依次通过访问如下链接:
http://localhost:8083/
http://localhost:8081/login?redirect=/indexHomePage
http://localhost:8082/
然后直接就可以不用登录就可以访问资源了,实现SSO功能
推荐阅读