JavaEE:CAS单点登录
说明:
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,将用户会话信息并存入RedisUserVO userVO = new UserVO();BeanUtils.copyProperties(user, userVO); //拷贝对象属性String token = jwtUtil.genToken(mobilePhone, code); //JWT生成tokenuserVO.setToken(token);redisTemplate.opsForValue().set("user_token:" + userVO.getId(), JsonUtil.objectToJson(userVO)); //存入用户信息json到redis会话//2.创建用户全局Ticket(标识用户在CAS系统已登录),存入Redis与CookieString userGlobalTicket = TicketGenerator.generateTGT(); //使用工具类生成全局票据//(1)以全局ticket为key,用户id为value,存入redisredisTemplate.opsForValue().set("user_global_ticket:" + userGlobalTicket, userVO.getId());//(2)将用户全局ticket存入cookieCookieUtil.putCookie(request, response, "user_global_ticket", userGlobalTicket, true);//3.创建用户临时Ticket(给用户子系统登录时进行单次验证), 存入RedisString userTempTicket = TicketGenerator.generateST(redirectUrl); //使用工具类根据返回url生成临时票据try {redisTemplate.opsForValue().set("user_temp_ticket:" + userTempTicket, MD5Util.md5(userTempTicket)); //将临时ticket作为key与value存入redisredisTemplate.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中用户全局ticketString userGlobalTicket = CookieUtil.getCookie(request, "user_global_ticket", true);if (StringUtils.isBlank(userGlobalTicket)) return Response.errorTicket("全局ticket已失效");//(2)根据用户全局ticket从redis中取出用户idString 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(给用户子系统登录时进行单次验证), 存入RedisString userTempTicket = TicketGenerator.generateST(redirectUrl); //使用工具类根据返回url生成临时票据try {redisTemplate.opsForValue().set("user_temp_ticket:" + userTempTicket, MD5Util.md5(userTempTicket)); //将临时ticket作为key与value存入redisredisTemplate.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中取出用户idString 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中用户全局ticketString userGlobalTicket = CookieUtil.getCookie(request, "user_global_ticket", true);CookieUtil.removeCookie(response, "user_global_ticket");//2.删除redis中用户idif (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 #模板的模式为HTMLencoding: 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(){}@Beanpublic CorsFilter corsFilter(){ //跨域家配置//1.添加cors配置CorsConfiguration config = new CorsConfiguration();config.setAllowedOrigins(Arrays.asList("http://www.yyh1.com", "http://www.yyh2.com")); //添加多个允许的域名config.setAllowCredentials(true); //允许发送cookieconfig.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 { //登录拦截器@Autowiredprotected RedisTemplate<String, Object> redisTemplate; //redis操作类@Overridepublic 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中是否存在有效ticketif (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登录页URLresponse.sendRedirect(casLoginUrl + "?redirectUrl=" + redirectUrl);return false;}return true;}
}
(2)配置拦截或不拦截的接口,实现WebMvcConfigurer:
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {@Beanpublic LoginInterceptor loginInterceptor(){ //自定义登录拦截器对象return new LoginInterceptor();}//添加登录拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) { //配置拦截或不拦截的接口registry.addInterceptor(loginInterceptor()) //加入自定义的拦截器类.addPathPatterns("/**") //拦截所有接口.excludePathPatterns("/loginCASSystem", "/verifyTempTicket", "/subSystemLogin", "/logout"); //对登录注册等账号相关接口放行}
}