基于SpringBoot實(shí)現(xiàn)單點(diǎn)登錄系統(tǒng)
來(lái)源: urlify.cn/I3eyAz
單點(diǎn)登錄系統(tǒng)設(shè)計(jì)思路:采用Spring4 Java配置方式整合HttpClient,Redis ,MySql和SpringBoot的簡(jiǎn)易教程。
在傳統(tǒng)的系統(tǒng),或者是只有一個(gè)服務(wù)器的系統(tǒng)中。Session在一個(gè)服務(wù)器中,各個(gè)模塊都可以直接獲取,只需登錄一次就進(jìn)入各個(gè)模塊。若在服務(wù)器集群或者是分布式系統(tǒng)架構(gòu)中,每個(gè)服務(wù)器之間的Session并不是共享的,這會(huì)出現(xiàn)每個(gè)模塊都要登錄的情況。這時(shí)候需要通過(guò)單點(diǎn)登錄系統(tǒng)(Single Sign On)將用戶(hù)信息存在Redis數(shù)據(jù)庫(kù)中實(shí)現(xiàn)Session共享的效果。從而實(shí)現(xiàn)一次登錄就可以訪(fǎng)問(wèn)所有相互信任的應(yīng)用系統(tǒng)。
一、整合 HttpClient
HttpClient 是 Apache Jakarta Common 下的子項(xiàng)目,用來(lái)提供高效的、最新的、功能豐富的支持 HTTP 協(xié)議的客戶(hù)端編程工具包,并且它支持 HTTP 協(xié)議最新的版本和建議。
首先在src/main/resources 目錄下創(chuàng)建 httpclient.properties 配置文件。
#設(shè)置整個(gè)連接池默認(rèn)最大連接數(shù) http.defaultMaxPerRoute=100 #設(shè)置整個(gè)連接池最大連接數(shù) http.maxTotal=300 #設(shè)置請(qǐng)求超時(shí) http.connectTimeout=1000 #設(shè)置從連接池中獲取到連接的最長(zhǎng)時(shí)間 http.connectionRequestTimeout=500 #設(shè)置數(shù)據(jù)傳輸?shù)淖铋L(zhǎng)時(shí)間 http.socketTimeout=10000
然后在 src/main/java/com/itdragon/config 目錄下創(chuàng)建 HttpclientSpringConfig.java 文件
這里用到了四個(gè)很重要的注解
@Configuration : 作用于類(lèi)上,指明該類(lèi)就相當(dāng)于一個(gè)xml配置文件
@Bean : 作用于方法上,指明該方法相當(dāng)于xml配置中的bean,注意方法名的命名規(guī)范
@PropertySource : 指定讀取的配置文件,引入多個(gè)value={“xxx:xxx”,“xxx:xxx”},ignoreResourceNotFound=true 文件不存在時(shí)忽略
@Value : 獲取配置文件的值
package com.itdragon.config;
/**
* @Configuration 作用于類(lèi)上,相當(dāng)于一個(gè)xml配置文件
* @Bean 作用于方法上,相當(dāng)于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();
// 最大連接數(shù)
poolingHttpClientConnectionManager.setMaxTotal(httpMaxTotal);
// 每個(gè)主機(jī)的最大并發(fā)數(shù)
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(httpDefaultMaxPerRoute); return poolingHttpClientConnectionManager;
}
@Bean // 定期清理無(wú)效連接
public IdleConnectionEvictor idleConnectionEvictor() { return new IdleConnectionEvictor(manager, 1L, TimeUnit.HOURS);
}
@Bean // 定義HttpClient對(duì)象 注意該對(duì)象需要設(shè)置scope="prototype":多例對(duì)象
@Scope("prototype")
public CloseableHttpClient closeableHttpClient() { return HttpClients.custom().setConnectionManager(this.manager).build();
}
@Bean // 請(qǐng)求配置
public RequestConfig requestConfig() { return RequestConfig.custom().setConnectTimeout(httpConnectTimeout) // 創(chuàng)建連接的最長(zhǎng)時(shí)間
.setConnectionRequestTimeout(httpConnectionRequestTimeout) // 從連接池中獲取到連接的最長(zhǎng)時(shí)間
.setSocketTimeout(httpSocketTimeout) // 數(shù)據(jù)傳輸?shù)淖铋L(zhǎng)時(shí)間
.build();
}
}
二、整合 Redis
SpringBoot官方其實(shí)提供了spring-boot-starter-redis pom 幫助我們快速開(kāi)發(fā),但我們也可以自定義配置,這樣可以更方便地掌控。
首先在src/main/resources 目錄下創(chuàng)建 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
設(shè)置Redis主機(jī)的ip地址和端口號(hào),和存入Redis數(shù)據(jù)庫(kù)中的key以及存活時(shí)間。這里為了方便測(cè)試,存活時(shí)間設(shè)置的比較小。這里的配置是單例Redis。
在src/main/java/com/itdragon/config 目錄下創(chuàng)建 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(){ // 省略第一個(gè)參數(shù)則是采用 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 目錄下創(chuàng)建 UserService.java 文件,它負(fù)責(zé)三件事情
第一件事情:驗(yàn)證用戶(hù)信息是否正確,并將登錄成功的用戶(hù)信息保存到Redis數(shù)據(jù)庫(kù)中。
第二件事情:負(fù)責(zé)判斷用戶(hù)令牌是否過(guò)期,若沒(méi)有則刷新令牌存活時(shí)間。
第三件事情:負(fù)責(zé)從Redis數(shù)據(jù)庫(kù)中刪除用戶(hù)信息。
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) {
// 檢查用戶(hù)名是否注冊(cè),一般在前端驗(yàn)證的時(shí)候處理,因?yàn)樽?cè)不存在高并發(fā)的情況,這里再加一層查詢(xún)是不影響性能的 if (null != userRepository.findByAccount(user.getAccount())) { return Result.build(400, "");
}
userRepository.save(user);
// 注冊(cè)成功后選擇發(fā)送郵件激活?,F(xiàn)在一般都是短信驗(yàn)證碼 return Result.build(200, "");
}
public Result userLogin(String account, String password,
HttpServletRequest request, HttpServletResponse response) {
// 判斷賬號(hào)密碼是否正確
User user = userRepository.findByAccount(account); if(user == null){ return Result.build(400, "賬號(hào)名或密碼錯(cuò)誤");
} if (!CheckUtils.decryptPassword(user, password)) { return Result.build(400, "賬號(hào)名或密碼錯(cuò)誤");
}
// 生成token
String token = UUID.randomUUID().toString();
// 清空密碼和鹽避免泄漏
String userPassword = user.getPassword();
String userSalt = user.getSalt();
user.setPassword(null);
user.setSalt(null);
// 把用戶(hù)信息寫(xiě)入 redis
jedisClient.set(REDIS_USER_SESSION_KEY + ":" + token, JsonUtils.objectToJson(user));
// user 已經(jīng)是持久化對(duì)象了,被保存在了session緩存當(dāng)中,若user又重新修改了屬性值,那么在提交事務(wù)時(shí),此時(shí) hibernate對(duì)象就會(huì)拿當(dāng)前這個(gè)user對(duì)象和保存在session緩存中的user對(duì)象進(jìn)行比較,如果兩個(gè)對(duì)象相同,則不會(huì)發(fā)送update語(yǔ)句,否則,如果兩個(gè)對(duì)象不同,則會(huì)發(fā)出update語(yǔ)句。
user.setPassword(userPassword);
user.setSalt(userSalt);
// 設(shè)置 session 的過(guò)期時(shí)間
jedisClient.expire(REDIS_USER_SESSION_KEY + ":" + token, SSO_SESSION_EXPIRE);
// 添加寫(xiě) cookie 的邏輯,cookie 的有效期是關(guān)閉瀏覽器就失效。
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) {
// 根據(jù)token從redis中查詢(xún)用戶(hù)信息
String json = jedisClient.get(REDIS_USER_SESSION_KEY + ":" + token);
// 判斷是否為空 if (StringUtils.isEmpty(json)) { return Result.build(400, "此session已經(jīng)過(guò)期,請(qǐng)重新登錄");
}
// 更新過(guò)期時(shí)間
jedisClient.expire(REDIS_USER_SESSION_KEY + ":" + token, SSO_SESSION_EXPIRE);
// 返回用戶(hù)信息 return Result.ok(JsonUtils.jsonToPojo(json, User.class));
}
}
四、Controller 層
負(fù)責(zé)跳轉(zhuǎn)登錄頁(yè)面跳轉(zhuǎn),負(fù)責(zé)用戶(hù)的登錄,退出,獲取令牌的操作。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,實(shí)際情況請(qǐng)和業(yè)務(wù)邏輯結(jié)合 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";
}
}
五、視圖層
一個(gè)簡(jiǎn)單的登錄頁(yè)面和資源展示頁(yè)面。login.jsp、index.jsp和indexHomePage.jsp
六、Spring 自定義攔截器
這里是另外一個(gè)項(xiàng)目 service-test-sso 中的代碼,首先在src/main/resources/spring/springmvc.xml 中配置攔截器,設(shè)置哪些請(qǐng)求需要攔截
"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) {
// 跳轉(zhuǎn)到登錄頁(yè)面,把用戶(hù)請(qǐng)求的url作為參數(shù)傳遞給登錄頁(yè)面。
response.sendRedirect("http://localhost:8081/login?redirect=" + request.getRequestURL());
// 返回false return false;
}
// 把用戶(hù)信息放入Request
request.setAttribute("user", user);
// 返回值決定handler是否執(zhí)行。true:執(zhí)行,false:不執(zhí)行。 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 {
}
}
七、操作步驟
測(cè)試思路:
第一步:注冊(cè)用戶(hù),執(zhí)行sso 項(xiàng)目下SpringbootStudyApplicationTests.java 單元測(cè)試類(lèi)中的 registerUser() 方法添加用戶(hù)。
第二步:開(kāi)啟sso服務(wù)。
第三步:再開(kāi)啟兩個(gè)service-test-sso服務(wù)。
第四步:在service-test-sso服務(wù)頁(yè)面點(diǎn)擊“訪(fǎng)問(wèn)主頁(yè)”按鈕進(jìn)入權(quán)限頁(yè)面測(cè)試。
八、sso項(xiàng)目結(jié)構(gòu)
service-test-sso項(xiàng)目結(jié)構(gòu)
訪(fǎng)問(wèn)主頁(yè)
點(diǎn)擊登錄
用戶(hù)表存儲(chǔ)如下
依次通過(guò)訪(fǎng)問(wèn)如下鏈接:
http://localhost:8083/
http://localhost:8081/login?redirect=/indexHomePage
http://localhost:8082/
然后直接就可以不用登錄就可以訪(fǎng)問(wèn)資源了,實(shí)現(xiàn)SSO功能
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀(guān)點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!