分布式商城redis应用,实现单点登录和购物车功能
单点登录+自定义注解+AOP
技术点:
redis+jsonp+cookie实现分布式系统内部的单点登录
抽取公共的js
$(function () {
$.ajax({
//url进行远程访问,跨域限制@CrossOriginal无法携带cookie所以只能通过jsonp实现
url:'http://localhost:8085/sso/isLogin',
// 后台返回的是一个js函数
dataType:'jsonp',
method:'post'
});
});
//直接调用取出data即可
function isLogin(data) {
//将json字符串转成json对象
var json = eval('(' + data + ')');
if(data != null){
$("#pid").html("您好"+json.username+",欢迎来到<b><a href=\"/\">ShopCZ商城</a></b> <a href=\"http://localhost:8085/sso/logout\" >注销</a>");
}else{
$("#pid").html("[<a href=\"javascript:toLogin()\" >登录</a>][<a >注册</a>]");
}
}
/**
* 在跳转前获取页面url
* location.href可以直接取值
* encodeURI()可以对地址栏的中文进行转码
* replace解决拼接多个参数的问题
*/
function toLogin(){
//获取本地url
var localUrl = location.href;
//对url进行编码
localUrl = encodeURI(localUrl);
//&拼接需要保证为一个参数需要手动编码
localUrl = localUrl.replace("&","%26");
location.href = "http://localhost:8085/sso/toLogin?returnUrl=" + localUrl;
}
redis+cookie实现登录
/**
* redis+jsonp+cookie实现分布式系统内部的单点登录
* 如果是多个系统无法通过cookie实现
* @Author 许恒亮
* @Time 2018/11/27 11:17
* @Version 1.0
*/
Controller
@RequestMapping("/sso")
public class SSOController {
@Reference
private IUserService userService;
@Autowired
private RedisTemplate redisTemplate;
/**
* returnUrl是跳转到登录页面前的页面url,带参数
* @param returnUrl
* @param model
* @return
*/
@RequestMapping("/toLogin")
public String toLogin(String returnUrl,Model model){
model.addAttribute("returnUrl",returnUrl);
return "login";
}
/**
* 登录功能
* 知识点:
* 1.ResultData在Service层对不同的结果设置不同的状态码自定义规则和错误信息
* 2.returnUrl在跳转到登录页面的时候将本地的url通过地址栏带参数的方式带到登录页面
* 然后作为隐藏域参数添加到登录后的页面跳转redirect+returnUrl实现对不同页面登录响应不同的结果
* 同时对地址栏进行编码,js解决中文乱码问题,encodeUri(returnUrl参数)
* 如果带有多个参数&需要手动进行替换 replace("&","%26")只有这样才能将url整个作为一个参数进行传递后跳转页面
* @param username
* @param password
* @param model
* @param response
* @param returnUrl
* @return
*/
@RequestMapping("/login")
public String login(String username, String password, Model model, HttpServletResponse response,String returnUrl){
//登陆
ResultData<User> resultData = userService.login(username, password);
switch (resultData.getCode()){
case 200:
//登陆成功
if(returnUrl == null || "".equals(returnUrl)){
returnUrl = "http://localhost:8082";
}
//将用户信息放到redis中
redisTemplate.opsForValue().set(Constant.LOGIN_TOKEN,resultData.getData());
//将token写到客户端cookie中
Cookie cookie = new Cookie("login_token",Constant.LOGIN_TOKEN);
cookie.setMaxAge(60 * 60 * 24 * 7);//设置过期时间
cookie.setPath("/");//设置cookie的有效路径
// cookie.setDomain();//设置cookie的有效域名,可以写二级域名,例如jd.com
// cookie.setHttpOnly();//设置cookie是否能被script等脚本访问
// cookie.setSecure();//设置cookie是否只支持https
response.addCookie(cookie);
model.addAttribute("user",resultData.getData());
return "redirect:" + returnUrl;
default:
//登录失败
model.addAttribute("error",resultData.getMessage());
return "login";
}
}
/**
* 验证是否登陆成功
* 只有jsonp才能带cookie,所以不能使用 @CrossOrigin
* @param token
* @return
*/
@RequestMapping("/isLogin")
@ResponseBody
public String checkLogin(@CookieValue(value = Constant.LOGIN_TOKEN,required = false) String token){
User user = null;
if(token != null){
user = (User)redisTemplate.opsForValue().get(token);
}
return user != null ? "isLogin('" + new Gson().toJson(user) + "')" : "isLogin(null)";
}
/**
* 注销功能
* 需要注意:把redis缓存中的数据情况还有cookie设为时间为0
* @param token
* @param response
* @return
*/
@RequestMapping("/logout")
public String logout(@CookieValue(value = Constant.LOGIN_TOKEN,required = false) String token, HttpServletResponse response){
if(token != null){
//清空redis
redisTemplate.delete(token);
//删除cookie
Cookie cookie = new Cookie(Constant.LOGIN_TOKEN, null);
cookie.setMaxAge(0);
//cookie可以重名,只要path不同,这时候是不同的cookie只通过名字是不能覆盖的
cookie.setPath("/");
response.addCookie(cookie);
}
return "login";
}
}
购物车实现
技术点:
AOP+自定义注解+redis+cookie
controller
@Controller
@RequestMapping("/cart")
public class CartController {
@IsLogin//默认不需要强制登录
@RequestMapping("/addCart")
public String addCart(Cart cart, User user){
System.out.println(cart);
System.out.println(user);
return "success";
}
}
自定义注解
/**
*
* 自定义注解
* 注解的声明:public @interface 注解名称
*
* 元注解:标记注解的注解
* @Documented:表示该注解会被javadoc命令写入api文档中
* @Target:注解的标记位置
* ElementType.ANNOTATION_TYPE:该注解可以标记别的注解
* ElementType.CONSTRUCTOR:标注到构造方法
* ElementType.FIELD:标注到成员属性
* ElementType.LOCAL_VARIABLE:标注到局部变量
* ElementType.METHOD:标注到方法上
* ElementType.PACKAGE:标注到包上
* ElementType.PARAMETER:标注到方法形参上
* ElementType.TYPE:标注到类、接口、枚举类上
* @Retention:注解的作用范围
* RetentionPolicy.SOURCE:注解的有效范围只在源码中,编译后就被丢弃
* RetentionPolicy.CLASS:注解有效范围在编译文件中,运行时丢弃
* RetentionPolicy.RUNTIME:注解在运行时仍然有效,这个范围的注解可以通过反射获取
*
* 注解内的方法声明:
* 类型 方法名() [defualt 默认值];
*
* 注意:
* 如果一个属性没有设置default默认值,在标记这个注解时,必须给定该属性值
* 如果一个属性的名字为value,则在赋值时可以省略属性名。当如果需要赋值两个以上的属性,则value不能省略
* 如果一个属性的类型是数组类型,则应该用{}赋值,如果只要给一个值,{}可以省略
*/
/**
* aop 实现
* @Author 许恒亮
* @Time 2018/11/27 16:12
* @Version 1.0
*/
@Target(ElementType.METHOD)//作用范围,方法上
@Retention(RetentionPolicy.RUNTIME)//运行时
public @interface IsLogin {
boolean value() default false;//是否强制需要登录
}
切面Aspect
/**
* @Author 许恒亮
* @Time 2018/11/27 16:17
* @Version 1.0
*/
@Aspect
public class LoginAspect {
@Autowired
private RedisTemplate redisTemplate;
/**
* 环绕增强
* 判断controller的方法上有没有@IsLogin注解
* 如果有就对其进行增强
* 增强效果
* 1/给方法的形参列表User user 注入值
* 如果登录了就注入user
* 如果没登录就注入null
* 2.如果IsLogin的value=false表示不强制跳转到登录页面
* 3.如果IsLogin的value=true表示强制跳转到登录页面,一旦发现cookie中没有值直接跳转不允许执行目标方法
*/
@Around("execution(* *..*Controller.*(..)) && @annotation(IsLogin)")
//表达式代表所有路径下面的xxxController类中的方法带@IsLogin注解的
public Object isLogin(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//获得request请求
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//通过reuqest获得cookie
Cookie[] cookies = request.getCookies();
//循环Cookie
String token = null;
if(cookies != null){
for (int i = 0; i < cookies.length; i++) {
//找到令牌对应的Cookie
if(cookies[i].getName().equals(Constant.LOGIN_TOKEN)){
//找到Key
token = cookies[i].getValue();
break;
}
}
}
User user = null;
if(token != null && !"".equals(token)){
//通过key去redis中找到用户信息
//有可能已经登录了
user = (User) redisTemplate.opsForValue().get(token);
}
if(user == null){
//没有登录---获得注解上的属性 判断返回情况 true就直接跳转到登录页面
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = methodSignature.getMethod();//获得方法对象
IsLogin isLoginAnnotation = method.getAnnotation(IsLogin.class);//isLogin注解对象
boolean flag = isLoginAnnotation.value();//获得isLogin注解的Value值
if(flag){//true直接跳转到登录页面
//因为登录后需要带returnUrl跳转到之前的页面,所以需要通过request对象获得url
StringBuffer requestURL = request.getRequestURL();
//带参数
requestURL.append(request.getQueryString());
//解决中文乱码问题
String url = URLEncoder.encode(requestURL.toString(), "utf-8");
//同样解决&带参数当做一个参数的问题
url = url.replace("&","%26");
return "redirect:http://localhost:8085/sso/toLogin?returnUrl=" + url;
}
//false继续执行目标方法
}
//表示已经登录或者不需要登录就可以继续操作
//获得形参列表
Object[] args = proceedingJoinPoint.getArgs();
if(args != null){
for (int i = 0; i < args.length; i++) {
if(args[i].getClass() == User.class){
//找到形参列表上的User user,并将从redis查到的user添加到形参列表
args[i] = user;
break;
}
}
}
//执行目标方法 --- 带指定的形参列表
Object result = proceedingJoinPoint.proceed(args);
//执行目标方法后的方法返回值就是目标方法的返回值
return result;
}
}
注册切面
@Bean//注册切面
public LoginAspect loginAspect(){
return new LoginAspect();
}