From 2d5478ef7fdd85d5d5921592e0fb4ecd1580ef58 Mon Sep 17 00:00:00 2001 From: click33 <2393584716@qq.com> Date: Sun, 11 Aug 2024 04:09:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3=E4=B8=8E=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20SpringSecurity=20=E5=8A=9F=E8=83=BD=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=AF=B9=E6=AF=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fun/auth-framework-function-test.md | 803 +++++++++++++++++- 1 file changed, 758 insertions(+), 45 deletions(-) diff --git a/sa-token-doc/fun/auth-framework-function-test.md b/sa-token-doc/fun/auth-framework-function-test.md index 3aa9a700..c319abd3 100644 --- a/sa-token-doc/fun/auth-framework-function-test.md +++ b/sa-token-doc/fun/auth-framework-function-test.md @@ -17,8 +17,46 @@ --- +### 依赖引入 -### 登录 & 注销 & 查询会话状态 + + + +``` xml + + + cn.dev33 + sa-token-spring-boot3-starter + 1.38.0 + +``` + + +``` xml + + + org.apache.shiro + shiro-spring-boot-web-starter + 1.13.0 + +``` + + +``` xml + + + org.springframework.boot + spring-boot-starter-security + 3.3.2 + +``` +SpringBoot 项目下一般不用特别指定 SpringSecurity 版本号 + + + + + +### 会话登录 & 会话状态查询 @@ -32,7 +70,7 @@ public class LoginController { @Autowired SysUserDao sysUserDao; - // 测试登录 ---- http://localhost:8081/acc/doLogin?username=zhang&password=123456 + // 测试登录 @RequestMapping("doLogin") public AjaxJson doLogin(String username, String password) { // 校验 @@ -49,7 +87,7 @@ public class LoginController { return AjaxJson.getSuccess("登录成功"); } - // 查询登录状态 ---- http://localhost:8081/acc/isLogin + // 查询登录状态 @RequestMapping("isLogin") public AjaxJson isLogin() { if(StpUtil.isLogin()) { @@ -58,13 +96,6 @@ public class LoginController { return AjaxJson.getError("未登录"); } - // 测试注销 ---- http://localhost:8081/acc/logout - @RequestMapping("logout") - public AjaxJson logout() { - StpUtil.logout(); - return AjaxJson.getSuccess("注销成功"); - } - } ``` @@ -157,13 +188,203 @@ public class LoginController { return AjaxJson.getError("未登录"); } - // 测试注销 ---- http://localhost:8082/acc/logout - @RequestMapping("logout") - public AjaxJson logout() { - SecurityUtils.getSubject().logout(); - return AjaxJson.getSuccess("注销成功"); +} +``` + + + +定义 SpringSecurity 配置类 +``` java +@Configuration +public class SpringSecurityConfigure { + + /** + * Spring Security的核心过滤器链配置 + */ + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + // 定义安全请求拦截规则 + httpSecurity.authorizeHttpRequests(router -> { + router + // 放行接口 + .requestMatchers("/acc/doLogin", "/acc/isLogin").permitAll() + + // 所有请求都需要认证 + .anyRequest().authenticated(); + ; + }); + + // 默认的表单登录 + httpSecurity.formLogin(withDefaults()); + + // 是否启用 csrf 防御 + httpSecurity.csrf( csrf -> csrf.disable() ); + + // 一些安全相关的全局响应头 + httpSecurity.headers(httpSecurityHeadersConfigurer -> { + httpSecurityHeadersConfigurer.cacheControl(HeadersConfigurer.CacheControlConfig::disable); + httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable); + }); + + return httpSecurity.build(); + } + + /** + * Spring Security 认证管理器 + */ + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + +} +``` + +定义 SpringSecurity UserDetails 管理器 +``` java +/** + * 自定义 SpringSecurity UserDetails 管理器 + * + * @author click33 + * @since 2024/8/8 + */ +@Component +public class CustomUserDetailsManager implements UserDetailsManager { + + @Autowired + SysUserDao sysUserDao; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser sysUser = sysUserDao.findByUsername(username); + if(sysUser == null){ + throw new UsernameNotFoundException("用户不存在"); + } + return User.withUsername(sysUser.getUsername()) + .password("{noop}" + sysUser.getPassword()) + .build(); + } + + @Override + public void createUser(UserDetails user) { + + } + + @Override + public void updateUser(UserDetails user) { + + } + + @Override + public void deleteUser(String username) { + + } + + @Override + public void changePassword(String oldPassword, String newPassword) { + + } + + @Override + public boolean userExists(String username) { + return false; + } + +} +``` + +测试 Controller + +``` java +@RestController +@RequestMapping("/acc/") +public class LoginController { + + @Autowired + AuthenticationManager authenticationManager; + + @Autowired + SysUserDao sysUserDao; + + // 测试登录 ---- http://localhost:8083/acc/doLogin?username=zhang&password=123456 + @RequestMapping("doLogin") + public AjaxJson doLogin(String username, String password, HttpServletRequest request) { + try { + // 验证账号密码 + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password); + usernamePasswordAuthenticationToken.setDetails(sysUserDao.findByUsername(username)); + Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken); + // 存入上下文 + SecurityContextHolder.getContext().setAuthentication(authentication); + request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); + // 返回 + return AjaxJson.getSuccess("登录成功!"); + } catch (Exception e) { + e.printStackTrace(); + return AjaxJson.getError(e.getMessage()); + } } + // 查询登录状态 ---- http://localhost:8083/acc/isLogin + @RequestMapping("isLogin") + public AjaxJson isLogin() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return AjaxJson.getSuccess("是否登录:" + !(authentication instanceof AnonymousAuthenticationToken)) + .set("principal", authentication.getPrincipal()) + .set("details", authentication.getDetails()); + } + +} + +``` + + + + + +### 会话注销 + + + + +``` java +@RequestMapping("logout") +public AjaxJson logout() { + StpUtil.logout(); + return AjaxJson.getSuccess("注销成功"); +} +``` + + + +``` java +@RequestMapping("logout") +public AjaxJson logout() { + SecurityUtils.getSubject().logout(); + return AjaxJson.getSuccess("注销成功"); +} +``` + + +``` java +@Bean +public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + + // 其它配置 ... + + // 注销相关配置 + httpSecurity.logout(logout -> { + logout.logoutUrl("/acc/logout"); + logout.logoutSuccessHandler((request, response, authentication) -> { + response.setStatus(200); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + String jsonStr = new ObjectMapper().writeValueAsString(AjaxJson.getSuccess("注销成功!")); + response.getWriter().write(jsonStr); + }); + }); + + return httpSecurity.build(); } ``` @@ -171,7 +392,7 @@ public class LoginController { -### 账号密码登录(加盐 MD5) +### 账号密码登录(MD5 加 salt) @@ -233,6 +454,44 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 登录代码照旧 + + +CustomUserDetailsManager 的 loadUserByUsername 指定 MD5 算法 + +``` java +@Override +public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser sysUser = sysUserDao.findByUsername(username); + if(sysUser == null){ + throw new UsernameNotFoundException("用户不存在"); + } + return User.withUsername(sysUser.getUsername()) + .password("{MD5}" + sysUser.getPassword()) + .build(); +} +``` + +登录时指定 salt + +``` java +@RequestMapping("doLogin") +public AjaxJson doLogin(String username, String password, HttpServletRequest request) { + try { + // 验证账号密码 + String salt = "abc"; + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, salt + password); + // 其它代码照旧 ... + + // 返回 + return AjaxJson.getSuccess("登录成功!"); + } catch (Exception e) { + e.printStackTrace(); + return AjaxJson.getError(e.getMessage()); + } +} +``` + + @@ -264,15 +523,32 @@ public AjaxJson getCurrUser() { } ``` + +``` java +// 从上下文获取当前登录 User 信息 +@RequestMapping("getCurrUser") +public AjaxJson getCurrUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if(!(authentication instanceof AnonymousAuthenticationToken)) { + SysUser sysUser = (SysUser)authentication.getDetails(); + return AjaxJson.getSuccess() + .set("id", sysUser.getId()) + .set("user", sysUser); + } + return AjaxJson.getError("未登录"); +} +``` + + -### 从 session 上存取值 +### 从会话上下文上存取值 ``` java -// 测试从 session 上存取值 +// 测试从从会话上下文存取值 @RequestMapping("testSession") public AjaxJson test() { SaSession session = StpUtil.getSession(); @@ -288,7 +564,7 @@ public AjaxJson test() { ``` java -// 测试从 session 上存取值 +// 测试从从会话上下文存取值 @RequestMapping("testSession") public AjaxJson test() { Subject subject = SecurityUtils.getSubject(); @@ -302,6 +578,23 @@ public AjaxJson test() { } ``` + + +``` java +// 测试从从会话上下文存取值 +@RequestMapping("testSession") +public AjaxJson testSession(HttpServletRequest request) { + HttpSession session = request.getSession(); + + System.out.println("从 session 上取值:" + session.getAttribute("name")); + session.setAttribute("name", "zhang"); + System.out.println("从 session 上取值:" + session.getAttribute("name")); + return AjaxJson.getSuccess(); +} +``` + + + @@ -345,9 +638,9 @@ public class JurController { @RequestMapping("assertRole") public AjaxJson assertRole() { // is 模式,返回 true 或 false - System.out.println("单个权限判断:" + StpUtil.hasRole("admin")); - System.out.println("多个权限判断(and):" + StpUtil.hasRoleAnd("admin", "dev-admin")); - System.out.println("多个权限判断(or):" + StpUtil.hasRoleOr("admin", "dev-admin")); + System.out.println("单个角色判断:" + StpUtil.hasRole("admin")); + System.out.println("多个角色判断(and):" + StpUtil.hasRoleAnd("admin", "dev-admin")); + System.out.println("多个角色判断(or):" + StpUtil.hasRoleOr("admin", "dev-admin")); // check 模式,无角色时抛出异常 StpUtil.checkRole("admin"); // 单个 check @@ -405,9 +698,9 @@ public class JurController { Subject subject = SecurityUtils.getSubject(); // is 模式,返回 true 或 false - System.out.println("单个权限判断:" + subject.hasRole("admin")); - System.out.println("多个权限判断(and):" + subject.hasAllRoles(Arrays.asList("admin", "dev-admin"))); - System.out.println("多个权限判断(or):" + (subject.hasRole("admin") || subject.hasRole("dev-admin"))); + System.out.println("单个角色判断:" + subject.hasRole("admin")); + System.out.println("多个角色判断(and):" + subject.hasAllRoles(Arrays.asList("admin", "dev-admin"))); + System.out.println("多个角色判断(or):" + (subject.hasRole("admin") || subject.hasRole("dev-admin"))); // check 模式,无角色时抛出异常 subject.checkRole("admin"); // 单个 check @@ -436,6 +729,58 @@ public class JurController { } ``` + + +CustomUserDetailsManager 的 loadUserByUsername 里返回用户的 角色 或 权限 信息 +``` java +@Override +public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser sysUser = sysUserDao.findByUsername(username); + if(sysUser == null){ + throw new UsernameNotFoundException("用户不存在"); + } + + // 不可以同时返回 roles 和 authorities,因为会相互覆盖,SpringSecurity 源码有bug + return User.withUsername(sysUser.getUsername()) + .password("{noop}" + sysUser.getPassword()) + // .roles("admin", "super-admin", "ceo") + .authorities("user:add", "user:delete", "user:update") + .build(); +} +``` + +测试 Controller +``` java +@RestController +@RequestMapping("/jur/") +public class JurController { + + // 角色判断 + @RequestMapping("assertRole") + public AjaxJson assertRole() { + SecurityExpressionRoot securityExpressionRoot = new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {}; + + System.out.println("单个角色判断:" + securityExpressionRoot.hasRole("admin")); + System.out.println("多个角色判断(and):" + (securityExpressionRoot.hasRole("admin") && securityExpressionRoot.hasRole("dev-admin"))); + System.out.println("多个角色判断(or):" + securityExpressionRoot.hasAnyRole("admin", "dev-admin")); + + return AjaxJson.getSuccess(); + } + + // 权限判断 + @RequestMapping("assertPermission") + public AjaxJson assertPermission() { + SecurityExpressionRoot securityExpressionRoot = new SecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication()) {}; + + System.out.println("单个权限判断:" + securityExpressionRoot.hasAuthority("user:add")); + System.out.println("多个权限判断(and):" + (securityExpressionRoot.hasAuthority("user:add") && securityExpressionRoot.hasAuthority("user:delete2"))); + System.out.println("多个权限判断(or):" + securityExpressionRoot.hasAnyAuthority("user:add", "user:delete2")); + + return AjaxJson.getSuccess(); + } + +} +``` @@ -528,6 +873,48 @@ public class AtCheckController { } ``` + + +`SpringSecurityConfigure` 配置类加上 `@EnableMethodSecurity` 注解 +``` java +@Configuration +@EnableMethodSecurity +public class SpringSecurityConfigure { + // ... +} +``` + +测试 Controller +``` java +@RestController +@RequestMapping("/at-check/") +public class AtCheckController { + + // 登录校验 + @PreAuthorize("isAuthenticated()") + @RequestMapping("checkLogin") + public AjaxJson checkLogin() { + return AjaxJson.getSuccess(); + } + + // 角色校验 + @PreAuthorize("hasRole('admin')") + @RequestMapping("checkRole") + public AjaxJson checkRole() { + return AjaxJson.getSuccess(); + } + + // 权限校验 + @PreAuthorize("hasAuthority('user:add')") + @RequestMapping("checkPermission") + public AjaxJson checkPermission() { + return AjaxJson.getSuccess(); + } + +} +``` + + @@ -550,7 +937,58 @@ public void addInterceptors(InterceptorRegistry registry) { } ``` -鉴权未通过时处理方案 + + +过滤器配置 +``` java +@Bean +public ShiroFilterFactoryBean shiroFilterFactoryBean() { + ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); + bean.setSecurityManager(securityManager()); + + // 路由拦截鉴权 + Map filterMap = new LinkedHashMap<>(); + filterMap.put("/route-check/getInfo", "anon"); // 不拦截 + filterMap.put("/route-check/getInfo2", "authc"); // 需要登录 + filterMap.put("/route-check/getInfo3", "perms[admin2]"); // 需要角色 + filterMap.put("/route-check/getInfo4", "perms[user:add3]"); // 需要权限 + bean.setFilterChainDefinitionMap(filterMap); + bean.setLoginUrl("/401"); // 未登录时跳转的 url + bean.setUnauthorizedUrl("/403"); // 未授权时跳转的 url + + return bean; +} +``` + + +SpringSecurityConfigure 配置 + +``` java +@Bean +public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + // 定义安全请求拦截规则 + httpSecurity.authorizeHttpRequests(router -> { + router + .requestMatchers("/route-check/getInfo1").permitAll() // 不拦截 + .requestMatchers("/route-check/getInfo2").authenticated() // 需要登录 + .requestMatchers("/route-check/getInfo3").hasRole("admin") // 需要 admin 角色 + .requestMatchers("/route-check/getInfo4").hasAuthority("user:add") // 需要 user:add 权限 + .anyRequest().permitAll(); // 所有请求都放行 + }); + + return httpSecurity.build(); +} +``` + + + + + +### 鉴权未通过的处理方案 + + + +定义全局异常处理类 ``` java @RestControllerAdvice public class GlobalException { @@ -580,15 +1018,9 @@ public class GlobalException { @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); - bean.setSecurityManager(securityManager()); - - // 路由拦截鉴权 - Map filterMap = new LinkedHashMap<>(); - filterMap.put("/route-check/getInfo", "anon"); // 不拦截 - filterMap.put("/route-check/getInfo2", "authc"); // 需要登录 - filterMap.put("/route-check/getInfo3", "perms[admin2]"); // 需要角色 - filterMap.put("/route-check/getInfo4", "perms[user:add3]"); // 需要权限 - bean.setFilterChainDefinitionMap(filterMap); + + // ... + bean.setLoginUrl("/401"); // 未登录时跳转的 url bean.setUnauthorizedUrl("/403"); // 未授权时跳转的 url @@ -596,7 +1028,7 @@ public ShiroFilterFactoryBean shiroFilterFactoryBean() { } ``` -鉴权未通过时处理方案 +定义路由 ``` java @RestController public class ShiroErrorController { @@ -616,6 +1048,66 @@ public class ShiroErrorController { } ``` + + +实现 `AccessDeniedHandler`, `AuthenticationEntryPoint` 接口 + +``` java +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler, AuthenticationEntryPoint, Serializable { + + // 未登录异常 + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + //验证为未登陆状态会进入此方法,认证错误 + response.setStatus(401); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + PrintWriter printWriter = response.getWriter(); + String body = "请先进行登录"; + printWriter.write(body); + printWriter.flush(); + } + + // 权限不足 + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { + // 登陆状态下,权限不足执行该方法 + response.setStatus(200); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + PrintWriter printWriter = response.getWriter(); + String body = "权限不足"; + printWriter.write(body); + printWriter.flush(); + } +} +``` + +注入 `SecurityFilterChain` + +``` java +// 未登录处理逻辑、权限不足处理逻辑 +@Autowired +private CustomAccessDeniedHandler accessDeniedHandler; + +@Bean +public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + + // 异常处理 + httpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> { + // 权限不足处理方案 + httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler); + // 未登录 处理逻辑 + httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(accessDeniedHandler); + }); + + return httpSecurity.build(); +} +``` + + + @@ -625,7 +1117,7 @@ public class ShiroErrorController { -pom.xml 依赖 +`pom.xml` 依赖 ``` xml @@ -635,7 +1127,7 @@ pom.xml 依赖 ``` -SaTokenConfigure 增加配置 Sa-Token 标签方言对象 +`SaTokenConfigure` 增加配置 `Sa-Token` 标签方言对象 ``` java // Sa-Token 标签方言 (Thymeleaf版) @@ -645,7 +1137,7 @@ public SaTokenDialect getSaTokenDialect() { } ``` -新建 ThymeleafConfigure 注入全局变量 +新建 `ThymeleafConfigure` 注入全局变量 ``` java @Configuration public class ThymeleafConfigure { @@ -657,7 +1149,7 @@ public class ThymeleafConfigure { } ``` -新建 Controller +新建 `Controller` ``` java @Controller public class HomeController { @@ -669,7 +1161,7 @@ public class HomeController { } ``` -新建 templates/index.html +新建 `templates/index.html` ``` html @@ -713,7 +1205,7 @@ public class HomeController { -pom.xml 依赖 +`pom.xml` 依赖 ``` xml @@ -723,7 +1215,7 @@ pom.xml 依赖 ``` -ShiroConfigure 增加配置 Shiro 方言对象 +`ShiroConfigure` 增加配置 `Shiro` 方言对象 ``` java @Bean public ShiroDialect shiroDialect() { @@ -731,7 +1223,7 @@ public ShiroDialect shiroDialect() { } ``` -新建 Controller +新建 `Controller` ``` java @Controller public class HomeController { @@ -744,7 +1236,7 @@ public class HomeController { } ``` -新建 templates/index.html +新建 `templates/index.html` ``` html @@ -784,6 +1276,75 @@ public class HomeController { ``` + + +`pom.xml` 引入依赖 +``` xml + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity6 + 3.1.2.RELEASE + +``` + +新建 `Controller` +``` java +@RestController +public class HomeController { + // 首页 + @RequestMapping("/") + public Object index(HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + request.setAttribute("isLogin", !(authentication instanceof AnonymousAuthenticationToken)); + return new ModelAndView("index.html"); + } +} +``` + +新建 `templates/index.html` + +``` html + + + + Shiro 集成 Thymeleaf 标签方言 + + + + +
+

Shiro 集成 Thymeleaf 标签方言 —— 测试页面

+

当前是否登录:

+ +

+ 登录 + 注销 +

+

登录之后才能显示:value

+

不登录才能显示:value

+ +

具有角色 admin 才能显示:value

+

同时具备多个角色才能显示:value

+

只要具有其中一个角色就能显示:value

+

不具有角色 admin 才能显示:value

+ +

具有权限 user-add 才能显示:value

+

同时具备多个权限才能显示:value

+

只要具有其中一个权限就能显示:value

+

不具有权限 user-add 才能显示:value

+ +

+ 当前登录账号: +

+ +
+ + +``` + + + @@ -912,6 +1473,10 @@ if(localStorage.token) { ``` + +见下方 “集成 Redis” 部分,同时做到:集成 Redis + 前后端分离。 + + @@ -1093,6 +1658,154 @@ public class SysUser implements Serializable { 其它代码照旧 + + + +(结合上部分,同时做到集成 Redis + 前后端分离) + +1、`pom.xml` 引入依赖 +``` xml + + + org.springframework.session + spring-session-data-redis + + + org.springframework.boot + spring-boot-starter-data-redis + +``` + +2、`yml` 增加配置 +``` yml +spring: + session: + store-type: redis + timeout: 8H + redis: + namespace: spring:session + + data: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 3 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 +``` + +3、在 `CustomAccessDeniedHandler` 自定义认证异常处理类中,返回 `json` 格式数据 + +``` java +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler, AuthenticationEntryPoint, Serializable { + + // 未登录异常 + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + //验证为未登陆状态会进入此方法,认证错误 + response.setStatus(401); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + PrintWriter printWriter = response.getWriter(); + String body = new ObjectMapper().writeValueAsString(AjaxJson.get(401, "请先进行登录")); + printWriter.write(body); + printWriter.flush(); + } + + // 权限不足 + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { + // 登陆状态下,权限不足执行该方法 + response.setStatus(200); + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + PrintWriter printWriter = response.getWriter(); + String body = new ObjectMapper().writeValueAsString(AjaxJson.get(403, "权限不足")); + printWriter.write(body); + printWriter.flush(); + } + +} +``` + +4、别忘了注入到 `SecurityFilterChain` 过滤器链 +``` java + // 异常处理 +httpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> { + // 权限不足处理方案 + httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler); + // 未登录 处理逻辑 + httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(accessDeniedHandler); +}); +``` + +5、在登录时,返回对应 token 信息 +``` java +// 测试登录 +@RequestMapping("doLogin") +public AjaxJson doLogin(String username, String password, HttpServletRequest request) { + try { + // 验证账号密码 + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password); + usernamePasswordAuthenticationToken.setDetails(sysUserDao.findByUsername(username)); + Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken); + // 存入上下文 + SecurityContextHolder.getContext().setAuthentication(authentication); + request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); + // 返回 + String token = request.getSession().getId(); + return AjaxJson.getSuccess("登录成功!").set("token", token); + } catch (Exception e) { + e.printStackTrace(); + return AjaxJson.getError(e.getMessage()); + } +} +``` + +6、前端改造 +- 1、在登录请求时,将返回的 token 保存到本地 `localStorage.setItem('token', res.token)`。 +- 2、在后续每次请求中,读取本地保存的 token 塞到请求 header 中 + +``` js +const header = {}; +if(localStorage.token) { + header.token = localStorage.token; +} +// 后续提交请求... +``` + +7、新建 `HttpSessionConfigure` 配置重写 `HttpSessionId` 读取策略,改为从 `header` 头读取 `token` 参数作为 `SessionId` +``` java +@Configuration +public class HttpSessionConfigure { + // HttpSession 读取策略,从 header 头读取 token 参数作为 session id + @Bean + public HeaderHttpSessionIdResolver httpSessionStrategy() { + System.out.println("----------------- 自定义 HttpSession Id 读取方式"); + return new HeaderHttpSessionIdResolver("token"); + } +} +``` + + +