【JavaWeb】认证授权(一)—— Session、JWT

1.基础知识

HTTP协议本身是无状态的,即使是同一台电脑同一个浏览器打开同一个页面两次,服务器不知道这两次请求是同一个客户端发送过来的,两次请求是完全独立的。例如,第一次请求时已经登录了,第二次再请求服务器会“忘了”你已经登录过。

为了解决这个问题,就有了CookieSession。它们的出现是为了让服务器“记住”之前这个客户端的一些数据,让HTTP保持状态

现在流行两种方式登录认证方式:SessionJWT,无论是哪种方式其原理都是Token机制,即保存凭证:
1.前端发起登录认证请求
2.后端登录验证通过,返回给前端一个凭证
3.前端发起新的请求时携带凭证

2.实现

@Data
public class User {
    private String username;
    private String password;
}

2.1Session

Session,是一种有状态的会话管理机制,其目的就是为了解决HTTP无状态请求带来的问题。

当用户登录认证请求通过时,服务端会将用户的信息存储起来,并生成一个Session Id发送给前端,前端将这个Session Id保存起来(一般是保存在Cookie中)。之后前端再发送请求时都携带Session Id,服务器端再根据这个Session Id来检查该用户有没有登录过。

在这里插入图片描述

2.1.1基本实现

首先准备一个实体类User

@Data
public class User {
    private String username;
    private String password;
}

准备一个UserController模拟登录

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/login")
    public String login(@RequestBody User user, HttpSession session) {
        // 模拟登录
        if ("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())) {
            session.setAttribute("user", user);
            return "登录成功!";
        }
        return "账号或密码错误!";
    }

    @GetMapping("/logout")
    public String logout(HttpSession session) {
        session.removeAttribute("user");
        return "退出成功!";
    }

    @GetMapping("/api")
    public String api(HttpSession session){
        // 模拟调用api
        User user = (User) session.getAttribute("user");
        if (user != null) {
            return "调用成功!";
        }
        return "请先登录!";
    }
}

使用ApiFox工具进行测试

如果用户第一次访问某个服务器时,服务器响应数据时会在响应头的Set-Cookie标识里将Session Id返回给浏览器,浏览器就将标识中的数据存在Cookie中。

未登录前调用其它API
在这里插入图片描述

第一次登录
在这里插入图片描述

浏览器Cookie中存储了SessionID

浏览器后续访问服务器就会携带Cookie
在这里插入图片描述
每一个Session Id都对应一个HttpSession对象,然后服务器就根据HttpSession对象来检测客户端是否已经登录了

前后端分离一般都是用ajax跨域请求后端数据,怎么携带cookie呢。这个很简单,只需要ajax请求时设置 withCredentials=true就可以跨域携带 cookie信息了


2.1.2添加过滤器

除了登录接口外,其他接口都要在Controller层里做登录判断,这太麻烦了。完全可以对每个接口过滤拦截一下,判断有没有登录,如果没有登录就直接结束请求,登录了才放行。

@Component
public class LoginFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String uri = request.getRequestURI();
        //如果是登录请求,直接放行
        if (uri.contains("login") ) {
            filterChain.doFilter(request, response);
        } else {
            //如果不是登录请求,判断是否登录
            if (request.getSession().getAttribute("user") == null) {
                //如果没有登录,返回未登录信息
                response.setContentType("application/json;charset=utf-8");
                PrintWriter out = response.getWriter();
                out.write("{\"code\":401,\"msg\":\"未登录\"}");
                out.flush();//清空缓冲区
                out.close();//关闭流
                return;
            }
            else {
                filterChain.doFilter(request, response);
            }
        }
    }
}

这样,我们就可以去除Controller层多余的登录判断逻辑了

@GetMapping("/api")
public String api(){
    return "调用成功!";
}

2.1.3上下文对象

在有些情况下,就算加了过滤器后还不能在controller层将session代码去掉。因为在实际业务中对用户对象操作是非常常见的,业务代码一般都写在Service层,那么我们Service层想要操作用户对象还得从Controller那传参过来。

@GetMapping("api")
public String api(HttpSession session) {
    User user = (User) session.getAttribute("user");
    // 将用户对象传递给Service层
    userService.doSomething(user);
    return "成功返回数据";
}

然而这样太麻烦了
我们可以通过SpringMVC提供的RequestContextHolder对象在程序任何地方获取到当前请求对象,从而获取我们保存在HttpSession中的用户对象。可以写一个上下文对象来实现该功能

public class RequestContext {
    public static HttpServletRequest getCurrentRequest() {
        // 通过`RequestContextHolder`获取当前request请求对象
        return ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
    }

    public static User getCurrentUser() {
        // 通过request对象获取session对象,再获取当前用户对象
        return (User)getCurrentRequest().getSession().getAttribute("user");
    }
}

2.2JWT

JWT:JSON Web Token
在这里插入图片描述
看起来和Session一样,其实,无论哪种方式其核心都是TOKEN机制。但,Session和JWT有一个重要的区别,就是Session是有状态的,JWT是无状态的

Session在服务端「保存了用户信息」。当前端携带Session Id到服务端时,服务端要检查其对应的HttpSession中有没有保存用户信息,保存了就代表登录了。
JWT在服务端「没有保存任何信息」。当使用JWT时,服务端只需要对这个字符串进行校验,校验通过就代表登录了。

2.2.1基本实现

工具类JwtUtil

public class JwtUtil {
    //密钥
    public static final String SECRET = "secret";
    //过期时间
    public static final long EXPIRE_TIME = 60 * 60 * 1000;

    /**
     * 生成token
     *
     * @param userName
     * @return 返回token字符串
     */
    public static String generate(String userName) {
        // 过期时间
        Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);

        return Jwts.builder()
                .setSubject(userName) // 将userName放进JWT
                .setIssuedAt(new Date()) // 设置JWT签发时间
                .setExpiration(expireDate)  // 设置过期时间
                .signWith(SignatureAlgorithm.HS512, SECRET) // 使用签名算法和密钥
                .compact();
    }

    /**
     * 校验token
     *
     * @param token
     * @return 成功返回Claims对象,失败返回null
     */
    public static Claims parse(String token) {
        if (token == null || token.equals("")) {
            return null;
        }
        // 这个Claims对象包含了许多属性,比如签发时间、过期时间以及存放的数据等
        Claims claims = null;
        try {   // 尝试解析token
            claims = Jwts.parser()
                    .setSigningKey(SECRET) // 设置秘钥
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            e.printStackTrace();// 解析失败:token无效
        }
        return claims;
    }
}

用户控制层

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/login")
    public String login(@RequestBody User user) {
        // 模拟登录
        if ("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())) {
            //如果登录成功,则返回生成的token,此时浏览器没有保存存储东西
            return JwtUtil.generate(user.getUsername());
        }
        return "账号或密码错误!";
    }


    @GetMapping("/api")
    public String api(HttpServletRequest request) {
        // 从请求头中拿到token
        String authorization = request.getHeader("Authorization");
        if (JwtUtil.parse(authorization) != null) {
            return "调用成功!";
        }
        return "请先登录!";
    }
}

可以看到,当用户第一次登录时,服务器返回了token
在这里插入图片描述

前端将token一般会放在请求头的Authorization项传递过来,其格式一般为类型 + token

我们在请求头中添加参数token,然后再发起请求
在这里插入图片描述

2.2.2添加拦截器

在这里插入图片描述
这里使用拦截器来实现前面的登录验证的过程

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //放行login请求
        if (request.getRequestURI().contains("login")) {
            return true;
        }
        // 从请求头中获取token字符串并解析
        Claims authorization = JwtUtil.parse(request.getHeader("Authorization"));
        // 已登录就直接放行
        if (authorization != null) {
            return true;
        }
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":401,\"msg\":\"未登录\"}");
        out.flush();// 必须刷新,否则前端可能接收不到数据
        out.close();// 关闭流
        return false;
    }
}

LoginJwtApplication 实现 WebMvcConfigurer 使拦截器生效

@SpringBootApplication
public class LoginJwtApplication implements WebMvcConfigurer {

    public static void main(String[] args) {
        SpringApplication.run(LoginJwtApplication.class, args);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 使拦截器生效
        registry.addInterceptor(new LoginInterceptor());
    }
}

2.2.3上下文对象

JWT不像Session把用户信息直接存储起来,所以JWT的上下文对象要靠自己来实现。

首先定义一个上下文类,这个类专门存储JWT解析出来的用户信息。要用到ThreadLocal,以防止线程冲突。

public final class UserContext {
    private static final ThreadLocal<String> user = new ThreadLocal<String>();

    public static void add(String userName) {
        user.set(userName);
    }

    public static void remove() {
        user.remove();
    }

    /**
     * @return 当前登录用户的用户名
     */
    public static String getCurrentUserName() {
        return user.get();
    }
}

修改拦截器代码如下

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //放行login请求
        if (request.getRequestURI().contains("login")) {
            return true;
        }
        // 从请求头中获取token字符串并解析
        Claims authorization = JwtUtil.parse(request.getHeader("Authorization"));
        // 已登录就直接放行
        if (authorization != null) {
            // 将我们之前放到token中的userName存到上下文对象中
            UserContext.add(authorization.getSubject());
            return true;
        }
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write("{\"code\":401,\"msg\":\"未登录\"}");
        out.flush();// 必须刷新,否则前端可能接收不到数据
        out.close();// 关闭流
        return false;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 请求结束后要从上下文对象删除数据,如果不删除则可能会导致内存泄露
        UserContext.remove();
    }
}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值