博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
基于SpringBoot实现单点登录系统
阅读量:2197 次
发布时间:2019-05-02

本文共 10111 字,大约阅读时间需要 33 分钟。

来源 |  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=200redis.node.host=10.128.15.21redis.node.port=6379REDIS_USER_SESSION_KEY=REDIS_USER_SESSIONSSO_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;@Controllerpublic 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 中配置拦截器,设置哪些请求需要拦截

 
 
 
  
  
  
 
 
 
  
   
   
  
 

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功能

推荐阅读GitHub 下载神器强势回归!巧用枚举来干掉if-else,代码更优雅!如何正确访问Redis中的海量数据?服务才不会挂掉!超硬核!1.6W 字 Redis 面试知识点总结,建议收藏!

转载地址:http://wvoub.baihongyu.com/

你可能感兴趣的文章
Spring源码剖析3:Spring IOC容器的加载过程
查看>>
Spring源码剖析4:懒加载的单例Bean获取过程分析
查看>>
Spring源码剖析5:JDK和cglib动态代理原理详解
查看>>
Spring源码剖析6:Spring AOP概述
查看>>
Spring源码剖析7:AOP实现原理详解
查看>>
Spring源码剖析8:Spring事务概述
查看>>
Spring源码剖析9:Spring事务源码剖析
查看>>
重新学习Mysql数据库1:无废话MySQL入门
查看>>
探索Redis设计与实现2:Redis内部数据结构详解——dict
查看>>
探索Redis设计与实现3:Redis内部数据结构详解——sds
查看>>
探索Redis设计与实现4:Redis内部数据结构详解——ziplist
查看>>
探索Redis设计与实现6:Redis内部数据结构详解——skiplist
查看>>
探索Redis设计与实现5:Redis内部数据结构详解——quicklist
查看>>
探索Redis设计与实现8:连接底层与表面的数据结构robj
查看>>
探索Redis设计与实现7:Redis内部数据结构详解——intset
查看>>
探索Redis设计与实现9:数据库redisDb与键过期删除策略
查看>>
探索Redis设计与实现10:Redis的事件驱动模型与命令执行过程
查看>>
分布式系统理论基础1: 一致性、2PC和3PC
查看>>
分布式系统理论基础2 :CAP
查看>>
分布式系统理论基础3: 时间、时钟和事件顺序
查看>>