说明:
CAS单点登录用于实现多个顶级域名不同的系统或各子系统实现统一登录,一处登录,各系统免登录。
JWT工具类实现:
JavaEE:JWT生成/解析token与Spring拦截器_jwt可以解析token吗-CSDN博客
一、CAS登录/登出实现:
1.单点登录(创建全局ticket+临时ticket):
/**
* 登录CAS系统(供CAS登录页调用)
* 1.登录验证,并创建用户分布式会话(Token存入Redis)
* (1)根据手机号从mysql库中查出用户信息
* (2)生成Token,将用户会话信息并存入Redis
* 2.创建用户全局Ticket(标识用户在CAS系统已登录),存入Redis与Cookie
* (1)以全局ticket为key,用户id为value,存入redis
* (2)将用户全局ticket存入cookie
* 3.创建用户临时Ticket(会过期,给用户登录子系统时进行单次验证),
* 4.重定向跳回登录之前的页面(携带用户临时Ticket)
*/
@GetMapping("/loginCASSystem")
public String loginCASSystem(String mobilePhone, String code, String redirectUrl, Model model, HttpServletRequest request, HttpServletResponse response) {
if (StringUtils.isBlank(mobilePhone) || StringUtils.isBlank(code)) {
//返回错误信息给前端页面,errorMessage与html页面中的th:text="${errorMessage}"一直
model.addAttribute("errorMessage", "手机号或验证码不能为空");
return "login"; //没登录过,跳转CAS登录页
}
//redis中的验证码是发送验证码时缓存的,一般有效期为几分钟
String redisCode = redisTemplate.opsForValue().get("user_phone_code:" + mobilePhone);
//将前端传递的验证码与redis中的验证码进行校验
if (StringUtils.isBlank(redisCode)) {
model.addAttribute("errorMessage", "请先获取验证码");
return "login"; //没登录过,跳转CAS登录页
}
//验证手机验证码是否正确
if (!redisCode.equals(code)) {
model.addAttribute("errorMessage", "验证码错误");
return "login"; //没登录过,跳转CAS登录页
}
//1.登录验证,并创建用户分布式会话(Token存入Redis)
//(1)根据手机号从mysql库中查出用户信息
User user = accountService.queryUserByPhone(mobilePhone);
if (user == null) {
model.addAttribute("errorMessage", "手机号未注册");
return "login"; //没登录过,跳转CAS登录页
}
//(2)生成Token,将用户会话信息并存入Redis
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO); //拷贝对象属性
String token = jwtUtil.genToken(mobilePhone, code); //JWT生成token
userVO.setToken(token);
redisTemplate.opsForValue().set("user_token:" + userVO.getId(), JsonUtil.objectToJson(userVO)); //存入用户信息json到redis会话
//2.创建用户全局Ticket(标识用户在CAS系统已登录),存入Redis与Cookie
String userGlobalTicket = TicketGenerator.generateTGT(); //使用工具类生成全局票据
//(1)以全局ticket为key,用户id为value,存入redis
redisTemplate.opsForValue().set("user_global_ticket:" + userGlobalTicket, userVO.getId());
//(2)将用户全局ticket存入cookie
CookieUtil.putCookie(request, response, "user_global_ticket", userGlobalTicket, true);
//3.创建用户临时Ticket(给用户子系统登录时进行单次验证), 存入Redis
String userTempTicket = TicketGenerator.generateST(redirectUrl); //使用工具类根据返回url生成临时票据
try {
redisTemplate.opsForValue().set("user_temp_ticket:" + userTempTicket, MD5Util.md5(userTempTicket)); //将临时ticket作为key与value存入redis
redisTemplate.expire("user_temp_ticket:" + userTempTicket, 5 * 60, TimeUnit.SECONDS); //设置临时Ticket过期时间(CAS官方默认值10秒)
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
//4.重定向跳回登录之前的页面(携带用户临时Ticket)
return String.format("redirect:%s?userTempTicket=%s", redirectUrl, userTempTicket);
}
2.验证临时ticket:
/**
* 验证临时ticket接口((给用户子系统登录时进行单次验证))
* 1.验证前端传递的用户临时ticket是否合法
* 2.删除redis中用户临时ticket(保证一次性)
* 3.取出cookie中用户全局ticket,根据用户全局ticket从redis中取出用户id,再根据用户id从redis中取出用户会话信息
* (1)取出cookie中用户全局ticket
* (2)根据用户全局ticket从redis中取出用户id
* (3)再根据用户id从redis中取出用户会话信息
*/
@PostMapping("/verifyTempTicket")
@ResponseBody
public Response verifyTempTicket(String userTempTicket, HttpServletRequest request, HttpServletResponse response) {
//1.验证前端传递的用户临时ticket是否合法
String redisUserTempTicket = redisTemplate.opsForValue().get("user_temp_ticket:" + userTempTicket);
if (StringUtils.isBlank(redisUserTempTicket) || !redisUserTempTicket.equals(userTempTicket)) return Response.errorTicket("无效的临时ticket");
//2.删除redis中用户临时ticket(保证一次性)
redisTemplate.delete("user_temp_ticket:" + userTempTicket);
//3.取出cookie中用户全局ticket,根据用户全局ticket从redis中取出用户id,再根据用户id从redis中取出用户会话信息
//(1)取出cookie中用户全局ticket
String userGlobalTicket = CookieUtil.getCookie(request, "user_global_ticket", true);
if (StringUtils.isBlank(userGlobalTicket)) return Response.errorTicket("全局ticket已失效");
//(2)根据用户全局ticket从redis中取出用户id
String userId = redisTemplate.opsForValue().get("user_global_ticket:" + userGlobalTicket);
if (StringUtils.isBlank(userId)) return Response.errorTicket("全局ticket已失效");
//(3)再根据用户id从redis中取出用户会话信息
String userJson = redisTemplate.opsForValue().get("user_token:" + userId);
if (StringUtils.isBlank(userJson)) return Response.errorTicket("会话已失效");
UserVO userVO = JsonUtil.jsonToObject(userJson, UserVO.class); //将用户会话json转为实体类
return Response.ok(userVO);
}
3.子系统二次登录:
/**
* 子系统登录使用(实现免登录效果)
* 1.验证用户是否登录过CAS系统
* 2.创建用户临时Ticket(给用户子系统登录时进行单次验证), 存入Redis
* 3.重定向跳回登录之前的页面(携带用户临时Ticket)
*/
@GetMapping("/subSystemLogin")
public String subSystemLogin(String redirectUrl, Model model, HttpServletRequest request, HttpServletResponse response) {
model.addAttribute("redirectUrl", redirectUrl);
//1.验证用户是否登录过CAS系统
String userGlobalTicket = CookieUtil.getCookie(request, "user_global_ticket", true);
if (!hasUserToken(userGlobalTicket)) return "login"; //没登录过,跳转CAS登录页
//2.创建用户临时Ticket(给用户子系统登录时进行单次验证), 存入Redis
String userTempTicket = TicketGenerator.generateST(redirectUrl); //使用工具类根据返回url生成临时票据
try {
redisTemplate.opsForValue().set("user_temp_ticket:" + userTempTicket, MD5Util.md5(userTempTicket)); //将临时ticket作为key与value存入redis
redisTemplate.expire("user_temp_ticket:" + userTempTicket, 5 * 60, TimeUnit.SECONDS); //设置临时Ticket过期时间(CAS官方默认值10秒)
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
//3.重定向跳回登录之前的页面(携带用户临时Ticket)
return String.format("redirect:%s?userTempTicket=%s", redirectUrl, userTempTicket);
}
//验证用户是否登录过CAS系统
private boolean hasUserToken(String userGlobalTicket) {
if (StringUtils.isBlank(userGlobalTicket)) return false; //用户全局ticket不存在
//根据用户全局ticket从redis中取出用户id
String userId = redisTemplate.opsForValue().get("user_global_ticket:" + userGlobalTicket);
if (StringUtils.isBlank(userId)) return false; //用户id不存在
//再根据用户id从redis中取出用户会话信息
String userJson = redisTemplate.opsForValue().get("user_token:" + userId);
if (StringUtils.isBlank(userJson)) return false; //用户会话不存在
return true;
}
4.登出:
/**
* 退出登录
*/
@PostMapping("/logout")
@ResponseBody
public Response logout(String userId, HttpServletRequest request, HttpServletResponse response) {
//1.清除cookie中用户全局ticket
String userGlobalTicket = CookieUtil.getCookie(request, "user_global_ticket", true);
CookieUtil.removeCookie(response, "user_global_ticket");
//2.删除redis中用户id
if (StringUtils.isNotBlank(userGlobalTicket)) {
redisTemplate.delete("user_global_ticket:" + userGlobalTicket); //删除redis中用户id
}
//删除redis中用户会话信息
redisTemplate.delete("user_token:" + userId);
return Response.ok();
}
二、使用模板实现CAS登录页(此处为了测试):
1.导入thymeleaf依赖:
<!-- 导入thymeleaf依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2.添加模板页配置,在模块工程/src/main/resources/application.yml中:
spring:
thymeleaf: #模板页配置
mode: HTML #模板的模式为HTML
encoding: UTF-8 #模板文件的编码格式
prefix: classpath:/template/ #模板文件的前缀路径
suffix: .html #模板文件的后缀名
3.创建模板HTML页面,在模块工程/src/main/resources/template目录下添加login.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CAS系统登录页</title>
<style>
.error {
color: red;
}
</style>
</head>
<body>
<form action="login" method="post">
<input type="text" name="username" placeholder="请输入用户名">
<input type="password" name="password" placeholder="请输入密码">
<input type="submit" value="登录">
<input type="hidden" name="redirectUrl" th:value="${redirectUrl}">
</form>
<!-- 显示CAS系统返回的错误 -->
<span class="error" th:text="${errorMessage}"></span>
</body>
</html>
三、其他配置:
1.允许跨域的配置:
@Configuration
public class CrossDomainConfig {
public CrossDomainConfig(){
}
@Bean
public CorsFilter corsFilter(){ //跨域家配置
//1.添加cors配置
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("http://www.yyh1.com", "http://www.yyh2.com")); //添加多个允许的域名
config.setAllowCredentials(true); //允许发送cookie
config.addAllowedMethod("*"); //允许所有方式的请求
config.addAllowedHeader("*"); //允许所有请求头
//2.为url添加映射路径
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source); //返回配置过的CorsFilter
}
}
2.配置登录拦截器:
(1)自定义拦截器类, 实现HandlerInterceptor:
@Component
public class LoginInterceptor implements HandlerInterceptor { //登录拦截器
@Autowired
protected RedisTemplate<String, Object> redisTemplate; //redis操作类
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//在接口请求处理之前校验token
// 1. 获取全局Ticket(优先从Cookie获取)
String userGlobalTicket = CookieUtil.getCookie(request, "user_global_ticket", true);
// 2. 验证Redis中是否存在有效ticket
if (StringUtils.isBlank(userGlobalTicket) || redisTemplate.opsForValue().get("user_global_ticket:" + userGlobalTicket) == null) {
// 3. 构建当前请求URL作为回调参数
String currentUrl = request.getRequestURL().toString();
String queryString = request.getQueryString();
String redirectUrl = currentUrl + (queryString != null ? "?" + queryString : "");
// 4. 跳转CAS登录页(携带当前URL作为redirectUrl参数)
String casLoginUrl = ""; //CAS登录页URL
response.sendRedirect(casLoginUrl + "?redirectUrl=" + redirectUrl);
return false;
}
return true;
}
}
(2)配置拦截或不拦截的接口,实现WebMvcConfigurer:
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Bean
public LoginInterceptor loginInterceptor(){ //自定义登录拦截器对象
return new LoginInterceptor();
}
//添加登录拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) { //配置拦截或不拦截的接口
registry.addInterceptor(loginInterceptor()) //加入自定义的拦截器类
.addPathPatterns("/**") //拦截所有接口
.excludePathPatterns("/loginCASSystem", "/verifyTempTicket", "/subSystemLogin", "/logout"); //对登录注册等账号相关接口放行
}
}