8、过滤器
我们登陆成功,就可以在登陆状态随意访问其他接口了,只需要验证请求头中的token是否合法,是否在redis里面有数据即可
下面开始编写filter
/** * @author: QiJingJing * @create: 2022/4/5 */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 获取token String token = request.getHeader("token"); if(!StringUtils.hasText(token)){ // 如果没有token则进行放行 filterChain.doFilter(request,response); return; } // 解析token String userId; try { Claims claims = JwtUtil.parseJwt(token); userId = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } // 从redis中获取用户信息 UserDetailsImpl userDetails = redisCache.getCacheObject("login" + userId); if(Objects.isNull(userDetails)){ throw new RuntimeException("用户未登录"); } // 存入SecurityContextHolder // 获取权限信息,封装到Authentication SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities())); // 放行 filterChain.doFilter(request,response); } }
然后把过滤器添加到配置类SecurityConfig
里面,继续添加以下代码
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Override protected void configure(HttpSecurity http) throws Exception { // 添加过滤器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } }
这个时候我们可以携带token信息访问hello接口,成功访问到信息
9、退出登陆(redis里面删除用户数据即可),对应的LoginController,LoginService、LoginServiceImpl
中添加以下代码
@RestController public class LoginController { @RequestMapping("user/logout") public ResponseResult logout(){ return loginService.logout(); } }
public interface LoginService { ResponseResult logout(); }
@Service public class LoginServiceImpl implements LoginService { @Override public ResponseResult logout() { // 获取SecurityContextHolder中的用户id UsernamePasswordAuthenticationToken authentication =(UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication(); UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal(); Long id = user.getUser().getId(); // 删除redis中的值 redisCache.deleteObject("login"+id); return new ResponseResult<>(200,"注销成功"); } }
测试注销
成功注销。
授权
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验,在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息来判断当前用户是否具有某种权限。
首先我们需要在配置类SecurityConfig
上开启相关配置,添加一个注解
@EnableGlobalMethodSecurity(prePostEnabled=true)
**具体实现:**这些权限我们要从数据库里面进行获取,基于RBAC
权限模型来实现。
RBAC:RBAC权限模型(Role-Based Access Control) 基于角色的权限控制。这是目前最被开发者使用也是相对易用、通用的权限模型
准备工作:创建所需的权限表、角色表、权限角色关联表、用户角色关联表
sql代码如下:
CREATE TABLE `sys_menu` ( `id` bigint NOT NULL AUTO_INCREMENT, `menuName` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名', `path` varchar(200) DEFAULT NULL COMMENT '路由地址', `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '组件路径', `visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示1隐藏)', `status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)', `perms` varchar(100) DEFAULT NULL COMMENT '权限标识', `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标', `createBy` bigint DEFAULT NULL, `createTime` datetime DEFAULT NULL, `updateBy` bigint DEFAULT NULL, `updateTime` datetime DEFAULT NULL, `delFalg` int DEFAULT '0' COMMENT '是否删除(0未删除,1已删除)', `remark` varchar(500) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='权限表';
CREATE TABLE `sys_role` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(128) DEFAULT NULL, `roleKey` varchar(100) DEFAULT NULL COMMENT '角色权限字符串', `status` char(1) DEFAULT '0' COMMENT '角色状态(0正常,1停用)', `createBy` bigint DEFAULT NULL, `createTime` datetime DEFAULT NULL, `updateBy` bigint DEFAULT NULL, `updateTime` datetime DEFAULT NULL, `delFalg` int DEFAULT '0' COMMENT '是否删除(0未删除,1已删除)', `remark` varchar(500) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
CREATE TABLE `sys_role_menu` ( `roleId` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID', `menuId` bigint NOT NULL DEFAULT '0' COMMENT '菜单id', PRIMARY KEY (`roleId`,`menuId`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关联表';
CREATE TABLE `sys_user_role` ( `userId` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', `roleId` bigint NOT NULL DEFAULT '0' COMMENT '角色id', PRIMARY KEY (`userId`,`roleId`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';
表中自行添加数据信息:
我们的perms
字段就是存储的权限信息字符串,只需要根据用户id把perms查询出来就可以。
编写Menu实体类:
@AllArgsConstructor @NoArgsConstructor @Data @TableName("sys_menu") public class Menu { @TableId private Long id; @TableField("menuName") private String menuName; @TableField("path") private String path; @TableField("component") private String component; @TableField("visible") private String visible; @TableField("status") private String status; @TableField("icon") private String icon; @TableField("createBy") private Long createBy; @TableField("createTime") private Date createTime; @TableField("updateBy") private Long updateBy; @TableField("updateTime") private Date updateTime; @TableField("delFlg") private int delFlg; @TableField("remark") private String remark; }
编写MenuMapper
@Mapper public interface MenuMapper extends BaseMapper<Menu> { List<String> selectPermsByUserId(@Param("userId") Long userId); }
编写MenuMapper.xml文件(resources/mapper/)目录下面
<?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.lili.mapper.MenuMapper"> <select id="selectPermsByUserId" resultType="String"> SELECT DISTINCT sm.`perms` FROM sys_user_role sur LEFT JOIN `sys_role` sr ON sur.`roleId` = sr.`id` LEFT JOIN `sys_role_menu` srm ON srm.`roleId` = sr.`id` LEFT JOIN `sys_menu` sm ON sm.`id` = srm.`menuId` WHERE userId = #{userId} AND sr.`status` = 0 AND sm.`status` = 0 </select> </mapper>
先在测试类测试是否可以正确查询到权限
@SpringBootTest class Springsecurity1ApplicationTests { @Autowired MenuMapper menuMapper; @Test void contextLoads() { menuMapper.selectPermsByUserId(1L).forEach(System.out::println); } }
结果:
system:dept:list system:text:list
到这一步,我们的sql代码就编写完成了,继续其他工作。
在UserDetailsServiceImpl
代码里面完成我们前面没有完成的查询用户权限的代码,补充之后完整代码如下:
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired UserMapper userMapper; @Autowired MenuMapper menuMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 查询用户信息 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUsername,username); User user = userMapper.selectOne(queryWrapper); // 如果没有用户,就抛出异常 if(Objects.isNull(user)){ throw new RuntimeException("用户名或者密码错误"); } // 查询用户对应的权限信息 List<String> permissions = menuMapper.selectPermsByUserId(user.getId()); // 把数据封装为UserDetails返回 return new UserDetailsImpl(user,permissions); } }
并且要把权限信息封装到UserDetailsImpl
对象中,所以完整UserDetailImpl代码如下
@Data @NoArgsConstructor public class UserDetailsImpl implements UserDetails { private User user; private List<String> permissions; /** * 不进行序列化 */ @JSONField(serialize = false) private List<GrantedAuthority> authorities; public UserDetailsImpl(User user,List<String> permissions){ this.user = user; this.permissions = permissions; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { if(authorities != null){ return authorities; } return permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
给我们需要的接口信息加上对应需要的权限
@RestController public class HelloController { @PreAuthorize("hasAuthority('system:dept:list')") @RequestMapping("/hello") public String hello(){ return "hello"; } }
运行测试:首先正常登陆用户,然后携带token去访问/hello接口
如果登陆成功,会将用户认证和权限信息存入redis,我们在获取redis里面数据的地方打个断点(JwtAuthenticationTokenFilter
类里面),看是否可以获取到权限信息
首先进行登陆
然后携带这个token,去访问hello接口
我们可以看到,reids里面有这个权限信息。放行即可
成功访问以上接口,我们可以继续测试一个没有对应权限的,比如id为2的用户,首先进行登陆
携带token进行访问:
可以看到这个用户的权限只有system:text:list
,没有system:dept:list
自然不能成功访问,我们放行即可
自然也就访问不了对应的数据了。
统一异常处理
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到,在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException调用AuthenticationEntryPoint
对象的方法去进行异常处理。
如果是授权过程出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler
对象的方法进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可
认证异常实现类
@Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"用户认证失败,请重新登陆"); String s = JSON.toJSONString(result); // 处理异常 WebUtils.renderString(response,s); } }
授权异常实现类
@Component public class AccessDeniedHandlerImpl implements org.springframework.security.web.access.AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "你的权限不足"); String s = JSON.toJSONString(result); WebUtils.renderString(response,s); } }
配置给
SpringSecurity
配置类,原有代码进行添加以下代码
@EnableGlobalMethodSecurity(prePostEnabled=true) @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired AccessDeniedHandlerImpl accessDeniedHandler; @Autowired AuthenticationEntryPointImpl authenticationEntryPoint; @Override protected void configure(HttpSecurity http) throws Exception { // 配置异常处理器 http.exceptionHandling() //认证失败过滤器 .authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler); } }
测试登陆失败
测试授权失败(例如id为2没有访问hello接口的权限)
跨域
浏览器出于安全考虑,使用XMLHttpRequest对象发起HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的,同源策略要求源相同才能正常通信,即协议、域名、端口号都完全一致。
前后端分离项目肯定会存在跨域请求的问题。所以需要进行配置处理
编写一个配置类
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 设置允许跨域的路径 registry.addMapping("/**") // 设置允许跨域请求的域名 .allowedOriginPatterns("*") // 是否允许cookie .allowCredentials(true) // 摄制允许的请求方式 .allowedMethods("GET","POST","DELETE","PUT") // 设置允许的header属性 .allowedHeaders("*") // 跨域访问时间 .maxAge(3600); } }
在SpringSecurity配置类开启跨域访问
原有代码继续添加以下配置
@EnableGlobalMethodSecurity(prePostEnabled=true) @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 允许跨域 http.cors(); } }