spring security(3)https://developer.aliyun.com/article/1531014
mapper
package com.sucurity.mapper; import java.util.List; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.sucurity.domain.Menu; import org.springframework.stereotype.Service; @Service //BaseMapper是mybatisplus官方提供的接口,里面提供了很多单表查询的方法 public interface MenuMapper extends BaseMapper<Menu> { //由于是多表联查,mybatisplus的BaseMapper接口没有提供,我们需要自定义方法,所以需要创建对应的mapper文件,定义对应的sql语句 List<String> selectPermsByUserId(Long id); }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.sucurity.mapper.MenuMapper"> <select id="selectPermsByUserId" resultType="java.lang.String"> SELECT DISTINCT m.`perms` FROM sys_user_role ur LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id` LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id` LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` WHERE user_id = #{userid} AND r.`status` = 0 AND m.`status` = 0 </select> </mapper>
修改返回的UserDetails
package com.sucurity.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.sucurity.domain.LoginUser; import com.sucurity.domain.User; import com.sucurity.mapper.MenuMapper; import com.sucurity.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; @Component public class UserDetailsServiceImpl implements UserDetailsService { @Autowired UserMapper userMapper; @Autowired private MenuMapper menuMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //查询用户信息 LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(User::getUserName, username); User user = userMapper.selectOne(lambdaQueryWrapper); //查询权限信息 if (user == null) { throw new UsernameNotFoundException("用户名不存在"); } LoginUser loginUser = new LoginUser(); loginUser.setUser(user); // 放入权限信息 List<String> list = menuMapper.selectPermsByUserId(user.getId()); loginUser.setPermissions(list); return loginUser; }
测试
修改一下
@RestController public class HelloController { @RequestMapping("/hello") @PreAuthorize("hasAuthority('system:test:list')") public String hello() { return "<h1>Hello World</h1>"; } }
四 ,自定义异常处理
上面的我们学习了 ‘认证’ 和 ‘授权’,实现了基本的权限管理,然后也学习了从数据库获取授权的 ‘授权-RBAC权限模型’,实现了从数据库获取用户具备的权限字符串。到此,我们完整地实现了权限管理的功能,但是,当认证或授权出现报错时,我们希望响应回来的json数据有实体类的code、msg、data这三个字段,怎么实现呢
我们需要学习Spring Security的异常处理机制,就可以在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到,如上图。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常,其中有如下两种情况
一、如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
二、如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
总结: 如果我们需要自定义异常处理,我们只需要创建AuthenticationEntryPoint和AccessDeniedHandler的实现类对象,然后配置给SpringSecurity即可
第AuthenticationEntryPointImpl类,写入如下,作用是自定义认证的实现类
package com.sucurity.hander; import com.alibaba.fastjson.JSON; import com.sucurity.domain.ResponseResult; import com.sucurity.utils.WebUtils; import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author 35238 * @date 2023/7/14 0014 15:51 */@Component //这个类只处理认证异常,不处理授权异常 public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override //第一个参数是请求对象,第二个参数是响应对象,第三个参数是异常对象。把异常封装成授权的对象,然后封装到handle方法 public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { //ResponseResult是我们在domain目录写好的实体类。HttpStatus是spring提供的枚举类,UNAUTHORIZED表示401状态码 ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "用户认证失败,请重新登录"); //把上面那行拿到的result对象转换为JSON字符串 String json = JSON.toJSONString(result); //WebUtils是我们在utils目录写好的类 String s = WebUtils.renderString(response, json); } }
在handler目录新建 AccessDeniedHandlerImpl 类,写入如下,作用是自定义授权的实现类
package com.sucurity.hander; import com.alibaba.fastjson.JSON; import com.sucurity.domain.ResponseResult; import com.sucurity.utils.WebUtils; import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component //这个类只处理授权异常,不处理认证异常 public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override //第一个参数是请求对象,第二个参数是响应对象,第三个参数是异常对象。把异常封装成认证的对象,然后封装到handle方法 public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { //ResponseResult是我们在domain目录写好的实体类。HttpStatus是spring提供的枚举类,FORBIDDEN表示403状态码 ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "您没有权限进行访问"); //把上面那行拿到的result对象转换为JSON字符串 String json = JSON.toJSONString(result); //WebUtils是我们在utils目录写好的类 WebUtils.renderString(response,json); } }
SecurityConfig 类修改为如下
package com.sucurity.config; import com.sucurity.filter.JwtAuthenticationTokenFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @EnableGlobalMethodSecurity(prePostEnabled = true) @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired //注入Security提供的认证失败的处理器,这个处理器里面的AuthenticationEntryPointImpl实现类,用的不是官方的了, //而是用的是我们在handler目录写好的AuthenticationEntryPointImpl实现类,因为我们也是添加到容器把官方的这个实现类覆盖了 private AuthenticationEntryPoint authenticationEntryPoint; @Autowired //注入Security提供的授权失败的处理器,这个处理器里面的AccessDeniedHandlerImpl实现类,用的不是官方的了, //而是用的是我们在handler目录写好的AccessDeniedHandlerImpl实现类,因为我们也是添加到容器把官方的这个实现类覆盖了 private AccessDeniedHandler accessDeniedHandler; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Autowired //注入我们在filter目录写好的类 private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Override protected void configure(HttpSecurity http) throws Exception { http //由于是前后端分离项目,所以要关闭csrf .csrf().disable() //由于是前后端分离项目,所以session是失效的,我们就不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() //指定让spring security放行登录接口的规则 .authorizeRequests() // 对于登录接口 anonymous表示允许匿名访问 .antMatchers("/user/login").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); //---------------------------认证过滤器的实现---------------------------------- //把token校验过滤器添加到过滤器链中 //第一个参数是上面注入的我们在filter目录写好的类,第二个参数表示你想添加到哪个过滤器之前 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //---------------------------异常处理的相关配置------------------------------- http.exceptionHandling() //配置认证失败的处理器 .authenticationEntryPoint(authenticationEntryPoint) //配置授权失败的处理器 .accessDeniedHandler(accessDeniedHandler); } }
五,跨域
1. 跨域的后端解决
由于我们的SpringSecurity负责所有请求和资源的管理,当请求经过SpringSecurity时,如果SpringSecurity不允许跨域,那么也是会被拦截,所以下面我们将学习并解决跨域问题。前面我们在测试时,是在postman测试,因此没有出现跨域问题的情况,postman只是负责发请求跟浏览器没关系
浏览器出于安全的考虑,使用 XMLHttpRequest 对象发起HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。 前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题
我们要实现如下两个需求 (我实际做出的效果跟教程视频不一致,第二个需求其实没必要存在,boot解决了跨域就都解决了):
1、开启SpringBoot的允许跨域访问
2、开启SpringSecurity的允许跨域访问
第一步: 开启SpringBoot的允许跨域访问。在 config 目录新建 CorsConfig 类,写入如下
package com.sucurity.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { @Override //重写spring提供的WebMvcConfigurer接口的addCorsMappings方法 public void addCorsMappings(CorsRegistry registry) { // 设置允许跨域的路径 registry.addMapping("/**") // 设置允许跨域请求的域名 .allowedOriginPatterns("*") // 是否允许cookie .allowCredentials(true) // 设置允许的请求方式 .allowedMethods("GET", "POST", "DELETE", "PUT") // 设置允许的header属性 .allowedHeaders("*") // 跨域允许时间 .maxAge(3600); } }
第二步: 开启SpringSecurity的允许跨域访问。在把 SecurityConfig 修改为如下。增加了一点代码,我会用红框框出来
package com.sucurity.config; import com.sucurity.filter.JwtAuthenticationTokenFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @EnableGlobalMethodSecurity(prePostEnabled = true) @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired //注入Security提供的认证失败的处理器,这个处理器里面的AuthenticationEntryPointImpl实现类,用的不是官方的了, //而是用的是我们在handler目录写好的AuthenticationEntryPointImpl实现类,因为我们也是添加到容器把官方的这个实现类覆盖了 private AuthenticationEntryPoint authenticationEntryPoint; @Autowired //注入Security提供的授权失败的处理器,这个处理器里面的AccessDeniedHandlerImpl实现类,用的不是官方的了, //而是用的是我们在handler目录写好的AccessDeniedHandlerImpl实现类,因为我们也是添加到容器把官方的这个实现类覆盖了 private AccessDeniedHandler accessDeniedHandler; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Autowired //注入我们在filter目录写好的类 private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Override protected void configure(HttpSecurity http) throws Exception { http //由于是前后端分离项目,所以要关闭csrf .csrf().disable() //由于是前后端分离项目,所以session是失效的,我们就不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() //指定让spring security放行登录接口的规则 .authorizeRequests() // 对于登录接口 anonymous表示允许匿名访问 .antMatchers("/user/login").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); //---------------------------认证过滤器的实现---------------------------------- //把token校验过滤器添加到过滤器链中 //第一个参数是上面注入的我们在filter目录写好的类,第二个参数表示你想添加到哪个过滤器之前 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //---------------------------异常处理的相关配置------------------------------- http.exceptionHandling() //配置认证失败的处理器 .authenticationEntryPoint(authenticationEntryPoint) //配置授权失败的处理器 .accessDeniedHandler(accessDeniedHandler); //---------------------------👇 设置security运行跨域访问 👇------------------ http.cors(); } }
spring security(5)https://developer.aliyun.com/article/1531017