分布式商城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();
    }