简介
springsecurity的授权,自定义授权失败的处理,跨域的处理和自定义权限校验方法的介绍
授权
权限系统作用
在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需的权限才能进行相应的操作,以此达到不同的用户可以使用不同的功能。
流程
会使用springsecurity默认的FilterSecurityInterceptor来进行权限校验,会从SecurityContextHolder获取其中的Authentication,从Authentication中获取权限的信息,判断当前用户是否拥有当前资源所需的权限。
实现方式
springsecurity提供了基于注解的权限控制方案,使用注解去指定访问对应的资源所需的权限。
需要在配置类中添加注解@EnableGlobalMethodSecurity
注解开启相关的配置
开启后,即可在controller的接口上添加使用springsecurity的权限相关的注解。如
@PreAuthorize("hasAuthority('权限字符串')")
:可以判断当前访问接口的用户是否有这个权限
数据库查询权限
rabc权限模型
基于角色的权限控制。
创建表
需要5张表
创建语句:
CREATE TABLE `sys_menu` (
`id` bigint NOT NULL AUTO_INCREMENT,
`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
`path` varchar(200) DEFAULT NULL COMMENT '路由地址',
`component` varchar(255) 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 '菜单图标',
`create_by` bigint DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`del_flag` int DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='权限表';
#################################
CREATE TABLE `sys_role` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL,
`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
`status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
`del_flag` int DEFAULT '0' COMMENT 'del_flag',
`create_by` bigint DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色表';
#################################
CREATE TABLE `sys_role_menu` (
`role_id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`menu_id` bigint NOT NULL DEFAULT '0' COMMENT '菜单id',
PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
#################################
CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
`email` varchar(64) DEFAULT NULL COMMENT '邮箱',
`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` varchar(128) DEFAULT NULL COMMENT '头像',
`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
`create_by` bigint DEFAULT NULL COMMENT '创建人的用户id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
#################################
CREATE TABLE `sys_user_role` (
`user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户id',
`role_id` bigint NOT NULL DEFAULT '0' COMMENT '角色id',
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
根据userid查询权限
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= 用户id
and r.`status` = 0
and m.`status` = 0
Menu 类
package com.springSecurityTest.common;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
@TableName(value="sys_menu") //指定表名,避免等下mybatisplus的影响
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
//Serializable是官方提供的,作用是将对象转化为字节序列
public class Menu implements Serializable {
private static final long serialVersionUID = -54979041104113736L;
@TableId
private Long id;
/**
* 菜单名
*/
private String menuName;
/**
* 路由地址
*/
private String path;
/**
* 组件路径
*/
private String component;
/**
* 菜单状态(0显示 1隐藏)
*/
private String visible;
/**
* 菜单状态(0正常 1停用)
*/
private String status;
/**
* 权限标识
*/
private String perms;
/**
* 菜单图标
*/
private String icon;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
/**
* 是否删除(0未删除 1已删除)
*/
private Integer delFlag;
/**
* 备注
*/
private String remark;
}
MenuMapper类
package com.springSecurityTest.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.springSecurityTest.common.Menu;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
List<String> selectMemusByUserId(Long userId);
}
menu.xml
<?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.springSecurityTest.mapper.MenuMapper">
<select id="selectMemusByUserId" resultType="java.lang.String">
select distinct sys_menu.perms from sys_user_role
left join sys_role
on sys_user_role.role_id = sys_role.id
left join sys_role_menu
on sys_user_role.role_id = sys_role_menu.role_id
left join sys_menu
on sys_menu.id = sys_role_menu.menu_id
where user_id = #{userid}
and sys_role.`status` = 0
</select>
</mapper>
springsecurity授权
UserDetailsServiceImpl类
把权限信息放入到loginuser中
package com.springSecurityTest.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.springSecurityTest.mapper.MenuMapper;
import com.springSecurityTest.mapper.UserMapper;
import com.springSecurityTest.common.LoginUser;
import com.springSecurityTest.common.User;
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.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
UserMapper userMapper;
@Resource
private MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getUserName,s);
User user = userMapper.selectOne(lambdaQueryWrapper);
if (Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
//查询权限
List<String> list = menuMapper.selectMemusByUserId(user.getId());
return new LoginUser(user,list);
}
}
LoginUser 类
添加权限的属性,重写getAuthorities方法。把permissions中的权限信息封装成simpleGrantauthority对象
package com.springSecurityTest.common;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
private List<String> permissions;
public LoginUser(User user,List<String> permissions){
this.user = user;
this.permissions = permissions;
}
@JSONField(serialize = false)
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!= null){
return authorities;
}
authorities = new ArrayList<>();
for(String permission:permissions){
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
authorities.add(simpleGrantedAuthority);
}
return authorities;
}
@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;
}
}
JwtAuthenticationTokenFilter 类
给usernamePasswordAuthenticationToken 对象添加权限
package com.springSecurityTest.filter;
import com.springSecurityTest.common.LoginUser;
import com.springSecurityTest.utils.JwtUtil;
import com.springSecurityTest.utils.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import sun.plugin.liveconnect.SecurityContextHelper;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = httpServletRequest.getHeader("token");
if (!StringUtils.hasText(token)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
String userId;
try {
System.out.println(JwtUtil.parseJWT(token));
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
String redisKey = "token:" + userId;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
controller类
package com.springSecurityTest.controller;
import com.springSecurityTest.common.User;
import com.springSecurityTest.mapper.UserMapper;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
public class dark {
@RequestMapping("/dark")
@PreAuthorize("@hasAuthority('system:dept:list')")
public String dark(){
return "it's too dark!";
}
}
自定义失败
在springsecurity中,如果在认证或者授权的过程中出现了异常,会被ExceptionTranslationFilter捕获,然后调用如下对象的方法处理异常:
- AuthenticationEntryPoint对象的方法会对认证过程中出现的异常进行处理
- AccessDeniedHandler对象的方法会对授权过程中出现的异常进行处理。
要自定义异常处理,只需要自定义AuthenticationEntryPoint和AccessDeniedHandler,然后配置给springsecurity。
AccessDeniedHandlerImpl类
package com.springSecurityTest.handler;
import com.alibaba.fastjson.JSON;
import com.springSecurityTest.common.ResponseResult;
import com.springSecurityTest.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Service;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Service
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足");
String json = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse,json);
}
}
AuthenticationEntryPointImpl类
package com.springSecurityTest.handler;
import com.alibaba.fastjson.JSON;
import com.springSecurityTest.common.ResponseResult;
import com.springSecurityTest.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Service;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Service
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "用户认证失败请重新登录");
String json = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse,json);
}
}
WebUtils
上面两个类中用到的工具类
package com.springSecurityTest.utils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class WebUtils {
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}
SecurityConfig
添加两个异常处理器
package com.springSecurityTest.config;
import com.springSecurityTest.filter.JwtAuthenticationTokenFilter;
import com.springSecurityTest.handler.AuthenticationEntryPointImpl;
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.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Resource
private AuthenticationEntryPointImpl authenticationEntryPoint;
@Resource
private AccessDeniedHandler accessDeniedHandler;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//testgit
@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()
.antMatchers("/dark").hasAuthority("system:test:list")
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);
//配置异常处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
http.cors();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
跨域问题
同源策略:协议,域名,端口号要一致
在应用中启用了Spring Security,它默认会对所有的请求进行拦截和验证。这意味着,即使Spring Boot配置允许了CORS,Spring Security的默认配置也可能阻止跨域请求,因为它会检查每一个请求是否带有有效的认证信息。
为了使Spring Security与CORS协同工作,通常需要在Spring Security的配置中显式地允许CORS请求。
springboot配置
package com.springSecurityTest.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配置
在springsecurity的配置类SecurityConfig中,重写configure方法,加上http.cors();
自定义权限校验方法
package com.springSecurityTest.expression;
import com.springSecurityTest.common.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.List;
@Component("MyExpressionRoot ")
public class MyExpressionRoot {
public boolean hasAuthority(String authority){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
return permissions.contains(authority);
}
}
使用自定义权限校验方法
package com.springSecurityTest.controller;
import com.springSecurityTest.common.User;
import com.springSecurityTest.mapper.UserMapper;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
public class dark {
@Resource
UserMapper userMapper;
@RequestMapping("/dark")
@PreAuthorize("@MyExpressionRoot .hasAuthority('system:dept:list')")
public String dark(){
return "it's too dark!";
}
@GetMapping("/getUser")
public List<User> usertest(){
List<User> users = userMapper.selectList(null);
System.out.println(users);
return users;
}
}
CSRF
跨站请求伪造,是web常见攻击之一,依靠的是cookie中携带的认证信息,使用token可以不用担心csrf攻击,因为token不存储在cookie中,而且前端把token设置到请求头中访问网站资源。
扩展
如果登录页面还有验证码,那还可以在UsernamePasswordAuthenticationFilter之前再写一个验证码的过滤器,组成过滤器链。