当前位置:首页 > > 架构师社区
[导读]单点登录系统设计思路:采用Spring4 Java配置方式整合HttpClient,Redis ,MySql和SpringBoot的简易教程。

来源: urlify.cn/I3eyAz


单点登录系统设计思路:采用Spring4 Java配置方式整合HttpClient,Redis ,MySql和SpringBoot的简易教程。
基于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() {
        ListjedisShardInfos = 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" />"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项目结构

基于SpringBoot实现单点登录系统

service-test-sso项目结构
基于SpringBoot实现单点登录系统

访问主页
基于SpringBoot实现单点登录系统

点击登录
基于SpringBoot实现单点登录系统
用户表存储如下
基于SpringBoot实现单点登录系统
依次通过访问如下链接:
http://localhost:8083/
http://localhost:8081/login?redirect=/indexHomePage
http://localhost:8082/
然后直接就可以不用登录就可以访问资源了,实现SSO功能


免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

特朗普集团近日取消了其新推出的T1智能手机“将在美国制造”的宣传标语,此举源于外界对这款手机能否以当前定价在美国本土生产的质疑。

关键字: 特朗普 苹果 AI

美国总统特朗普在公开场合表示,他已要求苹果公司CEO蒂姆·库克停止在印度建厂,矛头直指该公司生产多元化的计划。

关键字: 特朗普 苹果 AI

4月10日消息,据媒体报道,美国总统特朗普宣布,美国对部分贸易伙伴暂停90天执行新关税政策,同时对中国的关税提高到125%,该消息公布后苹果股价飙升了15%。这次反弹使苹果市值增加了4000多亿美元,目前苹果市值接近3万...

关键字: 特朗普 AI 人工智能 特斯拉

3月25日消息,据报道,当地时间3月20日,美国总统特朗普在社交媒体平台“真实社交”上发文写道:“那些被抓到破坏特斯拉的人,将有很大可能被判入狱长达20年,这包括资助(破坏特斯拉汽车)者,我们正在寻找你。”

关键字: 特朗普 AI 人工智能 特斯拉

1月22日消息,刚刚,新任美国总统特朗普放出重磅消息,将全力支持美国AI发展。

关键字: 特朗普 AI 人工智能

特朗普先生有两件事一定会载入史册,一个是筑墙,一个是挖坑。在美墨边境筑墙的口号确保边境安全,降低因非法移民引起的犯罪率过高问题;在中美科技产业之间挖坑的口号也是安全,美国企业不得使用对美国国家安全构成威胁的电信设备,总统...

关键字: 特朗普 孤立主义 科技产业

据路透社1月17日消息显示,知情人士透露,特朗普已通知英特尔、铠侠在内的几家华为供应商,将要撤销其对华为的出货的部分许可证,同时将拒绝其他数十个向华为供货的申请。据透露,共有4家公司的8份许可被撤销。另外,相关公司收到撤...

关键字: 华为 芯片 特朗普

曾在2018年时被美国总统特朗普称作“世界第八奇迹”的富士康集团在美国威斯康星州投资建设的LCD显示屏工厂项目,如今却因为富士康将项目大幅缩水并拒绝签订新的合同而陷入了僵局。这也导致富士康无法从当地政府那里获得约40亿美...

关键字: 特朗普 富士康

今年5月,因自己发布的推文被贴上“无确凿依据”标签而与推特发生激烈争执后,美国总统特朗普签署了一项行政令,下令要求重审《通信规范法》第230条。

关键字: 谷歌 facebook 特朗普

众所周知,寄往白宫的所有邮件在到达白宫之前都会在他地进行分类和筛选。9月19日,根据美国相关执法官员的通报,本周早些时候,执法人员截获了一个寄给特朗普总统的包裹,该包裹内包含蓖麻毒蛋白。

关键字: 美国 白宫 特朗普
关闭