# 认证与授权 **Repository Path**: yaodao666/AuthenticationAndAuthorization ## Basic Information - **Project Name**: 认证与授权 - **Description**: 认证与授权的几种实现方法:session,JWT,SpringBoot Security - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-09-12 - **Last Updated**: 2023-11-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Session方式 ```java package com.beerbear.session.controller; import com.beerbear.session.Entity.User; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpSession; @RestController public class SessionController { public String login(@RequestBody User user, HttpSession session){ // 判断账号密码是否正确,这一步肯定是要读取数据库中的数据来进行校验的,这里为了模拟就省去了 if ("admin".equals(user.getUsername()) && "admin".equals(user.getPassword())) { // 正确的话就将用户信息存到session中 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 "成功返回数据"; } @GetMapping("api2") public String api2(HttpSession session) { // 模拟各种api,访问之前都要检查有没有登录,没有登录就提示用户登录 User user = (User) session.getAttribute("user"); if (user == null) { return "请先登录"; } // 如果有登录就调用业务层执行业务逻辑,然后返回数据 return "成功返回数据"; } } ``` 简单实现。将user保存在session中。 ![image-20210912101814769](https://gitee.com/yaodao666/blog-image/raw/master/image-20210912101814769.png) `attributes`是个currentHashMap。 登录时: ![image-20210912102245730](https://gitee.com/yaodao666/blog-image/raw/master/image-20210912102245730.png) 再去调用api接口: ![image-20210912102410391](https://gitee.com/yaodao666/blog-image/raw/master/image-20210912102410391.png) session中确实有内容了。 为什么再次访问时session中会带内容?凭证的传递都是浏览器和Servlet帮我们做的。 https://juejin.cn/post/6866978103776772109 postman中将cookie信息删除后再调用就会失败。 **写过滤器**。用于简化Controller,不用获取HttpSession参数信息了,统一认证。 **上下文对象**。正常来说是让controller获取user后传给service层,为了让Service层获取到用户对象,都得这么干,这太麻烦了。所以写一个上下文对象,让所有应用代码随处可取user信息。 关键在于每次请求时,浏览器总会给session赋上曾经保存到cookie中的值。有人可能会问,前后端分离一般都是用`ajax`跨域请求后端数据,怎么携带cookie呢。这个很简单,只需要`ajax`请求时设置 `withCredentials=true`就可以跨域携带 cookie信息了 大彻大悟! 原来是第一次请求时会产生一个sessionId表示是来自哪个客户端请求,然后下次再去请求的时候就会带上这个id,服务端去看看这个id对应的session中[找session的过程应该是Servlet帮忙处理的]有没有这个user信息,有说明登陆过,允许该客户端保持登录状态。终于悟了! # JWT ![image-20210912105130210](https://gitee.com/yaodao666/blog-image/raw/master/image-20210912105130210.png) ```java @RestController public class JwtController { @PostMapping("/login") public String login(@RequestBody User user) { // 判断账号密码是否正确,这一步肯定是要读取数据库中的数据来进行校验的,这里为了模拟就省去了 if ("admin".equals(user.getUsername()) && "admin".equals(user.getPassword())) { // 如果正确的话就返回生成的token(注意哦,这里服务端是没有存储任何东西的) return JwtUtil.generate(user.getUsername()); } return "账号密码错误"; } } ``` ![image-20210912112240712](https://gitee.com/yaodao666/blog-image/raw/master/image-20210912112240712.png) **需要将生成的token返回给前端,前端要保存好这个token,下次再请求时带上。后端接收到这个token后,就进行解析,解析成功了就证明这个token有效,这中间后端并没有保存什么,只是验证这个token是不是我传给你的(密钥+协议)。** ```java @GetMapping("api") public String api(HttpServletRequest request) { // 从请求头中获取token字符串 String jwt = request.getHeader("Authorization"); // 解析失败就提示用户登录 if (JwtUtil.parse(jwt) == null) { return "请先登录"; } // 解析成功就执行业务逻辑返回数据 return "api成功返回数据"; } ``` ![image-20210912112438203](https://gitee.com/yaodao666/blog-image/raw/master/image-20210912112438203.png) 用一个字段来放置这个token就行。前后端只要约定好是哪个字段就行,上面是放在了Header中的Authorization字段中。 不携带token时(或者篡改token时): ![image-20210912112611676](https://gitee.com/yaodao666/blog-image/raw/master/image-20210912112611676.png) jwt就是利用一个协议+密钥+用户名生成一个密文给前端,前端利用密文来与后端通信(后端能够解析出用户信息),有点像谍战片里面的东西。 **拦截器,做统一认证操作,不用每个接口中都去做认证。** ```java package com.beerbear.jwt.Handler; import com.beerbear.jwt.Util.JwtUtil; import io.jsonwebtoken.Claims; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; public class LoginInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 简单的白名单,登录这个接口直接放行 if ("/login".equals(request.getRequestURI())) { return true; } // 从请求头中获取token字符串并解析 Claims claims = JwtUtil.parse(request.getHeader("Authorization")); // 已登录就直接放行 if (claims != null) { return true; } // 走到这里就代表是其他接口,且没有登录 // 设置响应数据类型为json(前后端分离) response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); // 设置响应内容,结束请求 out.write("请先登录"); out.flush(); out.close(); return false; } } ``` ```java package com.beerbear.jwt.Handler; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class InterceptorsConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 使拦截器生效 registry.addInterceptor(new LoginInterceptor()); } } ``` **上下文对象** 统一拦截做好之后接下来就是我们的上下文对象,`JWT`不像`Session`把用户信息直接存储起来,所以`JWT`的上下文对象要靠我们自己来实现。 首先我们定义一个上下文类,这个类专门存储`JWT`解析出来的用户信息。我们要用到`ThreadLocal`,以防止线程冲突: ```java package com.beerbear.jwt.Context; public final class UserContext { private static final ThreadLocal user = new ThreadLocal(); public static void add(String userName) { user.set(userName); } public static void remove() { user.remove(); } /** * @return 当前登录用户的用户名 */ public static String getCurrentUserName() { return user.get(); } } ``` 这里当然也可以设置一个`ThreadLocal`的对象。 ```java package com.beerbear.jwt.Context; import com.beerbear.jwt.Entity.User; public final class UserContext { private static final ThreadLocal user = new ThreadLocal(); public static void add(User u) { user.set(u); } public static void remove() { user.remove(); } /** * @return 当前登录用户的用户名 */ public static User getCurrentUser() { return user.get(); } } ``` ```java // 从请求头中获取token字符串并解析 Claims claims = JwtUtil.parse(request.getHeader("Authorization")); // 已登录就直接放行 if (claims != null) { String userName = claims.getSubject(); User user = findUserByUserName(userName); // 将用户信息放到上下文中 UserContext.add(user); return true; } ``` ![image-20210912114843383](https://gitee.com/yaodao666/blog-image/raw/master/image-20210912114843383.png) ![image-20210912114929424](https://gitee.com/yaodao666/blog-image/raw/master/image-20210912114929424.png) 问题JWT 如何做退出登录? ![image-20210912120849823](https://gitee.com/yaodao666/blog-image/raw/master/image-20210912120849823.png) # SpringBoot Security https://juejin.cn/post/6900721218207350791 依赖: ```xml org.springframework.boot spring-boot-starter-security ``` ![image-20210913091604326](https://gitee.com/yaodao666/blog-image/raw/master/20210913091612.png) ```java /** * @Author: Beer Bear * @Description: 自定义Security的配置 * @Date: 2021/9/13 9:16 */ @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { } ``` 自定义Security的配置。 ------ ## 最简单的认证方式 和Session一样的用法: `Authentication` `SecurityContext` `SecurityContextHolder` ```java Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); ``` ![image-20210913092122168](https://gitee.com/yaodao666/blog-image/raw/master/20210913092122.png) 对于Spring Security来说,这样确实就完成了认证,但对于我们来说还少了一步,那就是判断用户的账号密码是否正确。用户进行登录操作时从会传递过来账号密码,我们肯定是要查询用户数据然后判断传递过来的账号密码是否正确,只有正确了咱们才会将认证信息放到上下文对象中,不正确就直接提示错误: ```java // 调用service层执行判断业务逻辑 if (!userService.login(用户名, 用户密码)) { return "账号密码错误"; } // 账号密码正确了才将认证信息放到上下文中(用户权限需要再从数据库中获取,后面再说,这里省略) Authentication authentication = new UsernamePasswordAuthenticationToken(用户名, 用户密码, 用户的权限集合); SecurityContextHolder.getContext().setAuthentication(authentication); ``` 这样才算是一个完整的认证过程,和不使用安全框架时的流程是一样的哦,只是一些组件之前是我们自己实现的。 ```java package com.beerbear.springbootsecurity.controller; import com.beerbear.springbootsecurity.entity.User; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** * @Author: Beer Bear * @Description: 登录 * @Date: 2021/9/13 9:27 */ @RestController public class LoginController { /** * create by: Beer Bear * description: 登录 * create time: 2021/9/13 9:33 */ @PostMapping("/login") public String Login(@RequestBody User user){ if(user.getPassword().equals("admin") && user.getUsername().equals("admin")){ // 账号密码正确了才将认证信息放到上下文中(用户权限需要再从数据库中获取,后面再说,这里省略) Authentication authentication = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()); SecurityContextHolder.getContext().setAuthentication(authentication); return "登录成功!"; } return "账号密码错误"; } @GetMapping("api") public String api(){ return "调用api成功!"; } } ``` 启动时会产生一个password: ![image-20210913095950556](https://gitee.com/yaodao666/blog-image/raw/master/20210913095950.png) ![image-20210913095939818](https://gitee.com/yaodao666/blog-image/raw/master/20210913095939.png) ![image-20210913095934680](https://gitee.com/yaodao666/blog-image/raw/master/20210913095934.png) 加上这个密码就能成功访问接口了。 为啥调用api接口就行,login就不好使呢? ![image-20210913100258369](https://gitee.com/yaodao666/blog-image/raw/master/20210913100258.png) 终于找到了原因:[为啥Get可以但是Post不行及处理办法](https://www.cnblogs.com/xuruiming/p/13296312.html) 一些其他配置:https://segmentfault.com/a/1190000013057238 解决问题时又遇到了新问题: ```java @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin").password("admin").roles("ADMIN") .and() .withUser("terry").password("terry").roles("USER") .and() .withUser("larry").password("larry").roles("USER"); } } ``` ![image-20210913102823417](https://gitee.com/yaodao666/blog-image/raw/master/20210913102823.png) 登录时后台报错: ![image-20210913102857014](https://gitee.com/yaodao666/blog-image/raw/master/20210913102857.png) 错误:[There is no PasswordEncoder mapped for the id "null" ,解决](https://blog.csdn.net/syc000666/article/details/96862574) ```java @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) .withUser("admin").password(new BCryptPasswordEncoder().encode("admin")).roles("ADMIN") .and() .withUser("terry").password(new BCryptPasswordEncoder().encode("terry")).roles("USER") .and() .withUser("larry").password(new BCryptPasswordEncoder().encode("larry")).roles("USER"); } } ``` 还可以这样改(未验证):https://zhuanlan.zhihu.com/p/90404282 接下来再解决刚刚那个Post不能访问的问题: [为啥Get可以但是Post不行及处理办法](https://www.cnblogs.com/xuruiming/p/13296312.html) 除了刚刚这个方法,还有[进一步的解释](https://blog.csdn.net/caplike/article/details/106144789) 两种方式: - 关掉`CSRF`防御 - 使用`csrf-token` ①:重写`configure(HttpSecurity http)`方法,关闭`CSRF`防御 ```java @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { …… } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated().and().httpBasic().and().csrf().disable(); } } ``` ![image-20210913133746343](https://gitee.com/yaodao666/blog-image/raw/master/20210913133746.png) 果然这种方法是可行的,但是关闭防御显然不是好方法。 ②:实现自己的 `CsrfTokenRepository` 1. 对于放行的请求: 生成与用户绑定的缓存, 缓存 csrf-token; 2. 对于其他的请求: 需要携带第1步生成的 csrf-token, `CsrfFilter` 届时会用缓存中的和请求中的对比, 以判断是否是合法请求; 这个先不写(有点复杂稍微)。先往下学学看。 ## AuthenticationManager认证方式