[toc]
Redis存储会话
- 登录/注册 调用
redisOperator.set("userToken:" + users.getId(), token);
CookieUtils.setCookie(request,response,"user",JsonUtils.objectToJson(usersVo),true);
users.setToken(token);
- 退出登录 调用 前端删除cookie
redisOperator.del("userToken:" + userId);
SpringSession实现会话管理
- 引入依赖
<!-- spring-session依赖 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- yml 文件配置
spring:
session:
store-type: redis
- 开启
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class}) //否则强制登录
@MapperScan(basePackages = "com.njzy.mapper")////扫码mybatis mapper
@EnableScheduling // 开启定时任务
@EnableRedisHttpSession //开启使用Redis作为SpringSession
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- 耦合度高
拦截器配置
- 拦截器的类需要实现HandlerInterceptor
import com.njzy.result.Result;
import com.njzy.utils.JsonUtils;
import com.njzy.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
public class UserInterceptor implements HandlerInterceptor {
@Autowired
RedisOperator redisOperator;
/**
* 在controller调用之前拦截 在此拦截
*
* @return false: 请求被拦截 验证出现问题 true 校验通过
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("token");
String userId = request.getHeader("userId");
if (StringUtils.isBlank(token) || StringUtils.isBlank(userId)) {
returnErrorResponse(response, Result.error("验证失败"));
return false;
}
String value = redisOperator.get("userToken:" + userId);
if (!StringUtils.isBlank(value)) {
if (token.equals(value)) {
returnErrorResponse(response, Result.error("验证成功"));
} else {
returnErrorResponse(response, Result.error("token不匹配,其他客户端登录"));
return false;
}
}
returnErrorResponse(response, Result.error("验证失败"));
return true;
}
private void returnErrorResponse(HttpServletResponse response, Result result) {
OutputStream out = null;
try {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/json");
out = response.getOutputStream();
out.write(JsonUtils.objectToJson(result).getBytes("utf-8"));
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 在controller后 渲染视图前
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 在controller后 渲染视图后
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
- 注册拦截器
import com.njzy.interceptor.UserInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
// 配置knife4j 显示文档
registry.addResourceHandler("doc.html")
.addResourceLocations("classpath:/META-INF/resources/");
//配置swagger-ui显示文档
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");//swagger 映射 一旦配置必须设置
//公共文件
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.addResourceLocations("file:/Users/yuanfang/Downloads/images/");//本地路径资源地址
}
@Bean
public UserInterceptor userInterceptor() {
return new UserInterceptor();
}
/**
* 注册拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userInterceptor())
.addPathPatterns("/**")//匹配所有路径
.excludePathPatterns("/doc.html/**", "/swagger-resources", "/v3/api-docs",// 排除接口文档相关拦截
"/swagger-ui.html/**", "/webjars/**", "/error",
"/user/checkUsername", "/user/register", "/user/login");//排除不需要登录的拦截
WebMvcConfigurer.super.addInterceptors(registry);
}
}
- 如果页面空白 先放开拦截器查看都使用的那些请求 excludePathPatterns把请求过滤掉就行
CAS 系统实现单点登录
- 场景:多套独立的系统使用一套用户体系,在一个系统上登录后,其他系统无需登录,一个系统注销,其他系统需要重新登录
- 由多系统和cas系统组成
图1.png
- 前端部分代码展示
if (!userIsLogin) {
// 如果没有登录,判断一下是否存在临时票据
var tmpTicket = app.getUrlParam("tmpTicket");//从地址中取
if (tmpTicket != null && tmpTicket != "" && tmpTicket != undefined) {
// 如果有临时票据,就携带临时票据发起请求到cas验证获取用户会话
axios.defaults.withCredentials = true;
axios.post('http://127.0.0.1:8099/sso/verifyTmpTicket?tmpTicket=' + tmpTicket)
.then(res => {
if (res.data.code == 200) {
var userInfo = res.data.data;
this.userInfo = userInfo;
this.userIsLogin = true;
app.setCookie("user", JSON.stringify(userInfo));
window.location.href = "http://127.0.0.1:8099/sso-music/index.html";
}
});
} else {
// 如果没有临时票据,说明用户没登录过,可以跳转至cas做统一登录认证了
window.location.href = app.SSOServerUrl + "/sso/login?returnUrl=http://127.0.0.1:8080/sso-music/index.html";
}
-
对应的后端处理(此处有跨域自己处理下)
-
需要返回页面引用thymeleaf模板并配置yml文件
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
spring: # 配置登录模板页 thymeleaf: mode: HTML encoding: UTF-8 prefix: classpath:/templates/ suffix: .html
在resources下面新建templates文件夹里面存放登录页面login.html
单点登录controller代码
import com.njzy.pojo.vo.UsersVo; import com.njzy.pojo.bo.UsersBo; import com.njzy.result.Result; import com.njzy.service.UsersService; import com.njzy.utils.CookieUtils; import com.njzy.utils.JsonUtils; import com.njzy.utils.MD5Util; import com.njzy.utils.RedisOperator; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.UUID; @Controller // 需要返回页面 public class SSOController { @Autowired UsersService usersService; @Autowired RedisOperator redisOperator; //cookie的key值 存放的是全局的ticket 一般需要加密处理 private final String COOK_NAME = "cookie_user_ticket"; //存放分布式会话的key前缀 拼接用户id组成key 里面存放用户信息 private final String USER_TOKEN = "userToken"; //key 前缀 + 全局门票 value 里面存放用户ID private final String USER_TICKET = "userTicket"; @GetMapping("/login") public String sso(String returnUrl, Model model, HttpServletRequest request, HttpServletResponse response) { model.addAttribute("returnUrl", returnUrl); //获取ticket 如果cookie中有说明用户登录过 此时发放一次tempTicket String ticket = CookieUtils.getCookieValue(request, COOK_NAME); if (verifyTicket(ticket)) { String tempTicket = UUID.randomUUID().toString().trim(); redisOperator.set("useTempTicket:" + tempTicket, MD5Util.md5(tempTicket), 600); return "redirect:" + returnUrl + "?tempTicket=" + tempTicket; } //用户未登录 统一跳转CAS登录页面 return "login"; } /** * 校验全局ticket */ private boolean verifyTicket(String ticket) { if (StringUtils.isBlank(ticket)) { return false; } // 验证 cas门票是否有效 String userId = redisOperator.get(USER_TICKET + ":" + ticket); if (StringUtils.isBlank(userId)) { return false; } // 验证会话是否存在 String s = redisOperator.get(USER_TOKEN + ":" + userId); if (StringUtils.isBlank(s)) { return false; } return true; } /** * CAS 统一登录接口 * 创建用户全局会话 token * 创建用户全局门票 用于表示在cas端是否登录 ticket * 创建用户临时票据 用于回跳回传 一次性 tempTicket */ @PostMapping("/doLogin") public String doLogin(String username, String password, String returnUrl, Model model, HttpServletRequest request, HttpServletResponse response) { model.addAttribute("returnUrl", returnUrl); if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { model.addAttribute("errmsg", "用户名密码不能为空"); return "login"; } else { UsersBo usersBo = new UsersBo(); usersBo.setUsername(username); usersBo.setPassword(password); // 1.登录 UsersVo result = usersService.doLogin(usersBo, request, response); if (result != null) { //2.通过redis实现用户会话 String token = UUID.randomUUID().toString().trim(); UsersVo usersVo = new UsersVo(); BeanUtils.copyProperties(result, usersVo); usersVo.setToken(token); redisOperator.set(USER_TOKEN + ":" + result.getId(), JsonUtils.objectToJson(usersVo)); } else { model.addAttribute("errmsg", "用户名密码不正确"); return "login"; } // 3。生成全局ticket门票 并放在CAS端的cookie中 String ticket = UUID.randomUUID().toString().trim(); CookieUtils.setCookie(request, response, COOK_NAME, ticket); // 4、全局ticket关联用户id 并存放到redis redisOperator.set(USER_TICKET + ":" + ticket, result.getId()); // 5、生成临时ticket 回跳前端网址 String tempTicket = UUID.randomUUID().toString().trim(); redisOperator.set("useTempTicket:" + tempTicket, MD5Util.md5(tempTicket), 600); return "redirect:" + returnUrl + "?tempTicket=" + tempTicket; } } @PostMapping("/verifyTmpTicket") @ResponseBody public Result verifyTmpTicket(String tmpTicket, HttpServletRequest request, HttpServletResponse response) { //校验用户是否登录 String tmpTicketValue = redisOperator.get("useTempTicket:" + tmpTicket); if (StringUtils.isBlank(tmpTicketValue)) { return Result.error(("用户票据错误")); } if (!tmpTicketValue.equals(MD5Util.md5(tmpTicket))) { return Result.error(("用户票据错误")); } else { //校验成功 后需要销毁票据 redisOperator.del("useTempTicket:" + tmpTicket); } return Result.success(); } @PostMapping("/logout") @ResponseBody public Result logout(String userId, HttpServletRequest request, HttpServletResponse response) { String ticket = CookieUtils.getCookieValue(request, COOK_NAME); // 删除 cookie和redis中的票据 CookieUtils.deleteCookie(request, response, COOK_NAME); redisOperator.del(USER_TICKET + ":" + ticket); //清除会话信息 redisOperator.del(USER_TOKEN + ":" + userId); return Result.success(); } }
-