目录
写在前面:强烈推荐查看大佬的文章13.Spring security权限管理
1.1 建立用户表、角色表和路径表,并在数据库中插入对应关系
写在前面:强烈推荐查看大佬的文章13.Spring security权限管理
1.基于url的权限管理
1.1 建立用户表、角色表和路径表,并在数据库中插入对应关系
CREATE DATABASE SECURITY;
USE SECURITY;
CREATE TABLE USER(
id INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(100) NOT NULL UNIQUE,
`password` VARCHAR(100),
role INT(11) NOT NULL DEFAULT 2,
email VARCHAR(50),
enabled TINYINT(1) DEFAULT 1,
locked TINYINT(1) DEFAULT 0
)ENGINE INNODB DEFAULT CHARSET=utf8;
CREATE TABLE `menu` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`pattern` VARCHAR(128) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
CREATE TABLE `role` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(32) DEFAULT NULL,
`nameZh` VARCHAR(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
CREATE TABLE `menu_role` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`mid` INT(11) DEFAULT NULL,
`rid` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
CREATE TABLE `user_role` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`uid` INT(11) DEFAULT NULL,
`rid` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
INSERT INTO `menu` (`id`, `pattern`)
VALUES
(1,'/admin/**'),
(2,'/user1/**'),
(3,'/visitor/**');
INSERT INTO `role` (`id`, `name`, `nameZh`)
VALUES
(1,'ROLE_ADMIN','系统管理员'),
(2,'ROLE_USER','普通用户'),
(3,'ROLE_VISITOR','游客');
INSERT INTO `user` (`id`, `username`, `password`, `role`,`enabled`, `locked`)
VALUES
(1,'admin','admin',1,1,0),
(2,'user','user',2,1,0),
(3,'visitor','visitor',3,1,0);
#第一个路径只能admin访问,第二个路径只能user访问,第三个路径只能vistor和user访问
INSERT INTO `menu_role` (`id`, `mid`, `rid`)
VALUES
(1,1,1),
(2,2,2),
(3,3,3),
(4,3,2);
#用户中role为1的既是管理员又是用户,role为2的为用户,role为3的是游客
INSERT INTO `user_role` (`id`, `uid`, `rid`)
VALUES
(1,1,1),
(2,1,2),
(3,2,2),
(4,3,3);
在security中,角色具有继承性,当一个用户同时拥有两个角色role时,他也同时具有两个role的访问权限。
1.2 添加实体类
user类需要实现security的UserDetails接口,将用户对应的角色告诉security。该代码还实现的JSR303的参数校验,其中TinyInt(1)会被mybatis-plus自动转成boolean型,无需自定义注释。
package com.cg.springSecurity.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cg.springSecurity.valid.AddGroup;
import com.cg.springSecurity.valid.UpdateGroup;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author cg
* @since 2022-05-13
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("user")
public class User implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@NotBlank(message = "用户名不能为空" , groups = {AddGroup.class})
private String username;
@NotBlank(message = "密码不能为空", groups = {AddGroup.class, UpdateGroup.class})
private String password;
@Email(message = "邮箱格式不正确!" , groups = AddGroup.class)
private String email;
private Integer role;
private boolean enabled;
private boolean locked;
//一个用户有多个角色
@TableField(exist = false)
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !locked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
package com.cg.springSecurity.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* @author cg
* @date 2023/3/29 10:52
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("role")
public class Role implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String name;
private String nameZh;
}
package com.cg.springSecurity.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* @author cg
* @date 2023/3/29 10:55
*/
@Data
@TableName("menu")
public class Menu implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Integer id;
private String pattern;
@TableField(exist = false)
//一个路径可以被多个角色访问
private List<Role> roles;
}
1.3 创建mapper
package com.cg.springSecurity.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cg.springSecurity.pojo.Role;
import com.cg.springSecurity.pojo.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author cg
* @since 2022-05-13
*/
public interface UserMapper extends BaseMapper<User> {
User selectByName(@Param("name") String name);
List<Role> getUserRolesByRole(@Param("role") Integer role);
User getUserById(@Param("id") Integer 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.cg.springSecurity.mapper.UserMapper">
<select id="selectByName" resultType="com.cg.springSecurity.pojo.User">
SELECT * FROM security.user WHERE username = #{name};
</select>
<select id="getUserRolesByRole" resultType="com.cg.springSecurity.pojo.Role">
SELECT r.*
FROM security.role r,security.user_role ur
WHERE ur.uid = #{role} AND ur.`rid`=r.`id`;
</select>
<resultMap id="UserInfoMap" type="com.cg.springSecurity.pojo.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
<result property="enabled" column="enabled"/>
<result property="locked" column="locked"/>
<collection property="roles" ofType="com.cg.springSecurity.pojo.Role">
<id column="rid" property="id"/>
<result column="name" property="name"/>
<result column="nameZh" property="nameZh"/>
</collection>
</resultMap>
<select id="getUserById" resultMap="UserInfoMap">
SELECT u.* ,r.id as rid ,r.name ,r.nameZh
FROM security.role r,security.user_role ur,security.`user` u
WHERE u.role = #{id} AND ur.`rid`=r.`id` AND ur.`uid`=u.`id`;
</select>
</mapper>
package com.cg.springSecurity.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cg.springSecurity.pojo.User;
/**
* <p>
* Mapper 接口
* </p>
*
* @author cg
* @since 2022-05-13
*/
public interface RoleMapper extends BaseMapper<User> {
}
package com.cg.springSecurity.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cg.springSecurity.pojo.Menu;
import com.cg.springSecurity.pojo.User;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author cg
* @since 2022-05-13
*/
public interface MenuMapper extends BaseMapper<User> {
/**
* @return 每一条路径中能访问该路径的所有角色
*/
List<Menu> getAllMenu();
}
<?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.cg.springSecurity.mapper.MenuMapper">
<resultMap id="MenuInfoMap" type="com.cg.springSecurity.pojo.Menu">
<id property="id" column="mid"/>
<result property="pattern" column="pattern"/>
<collection property="roles" ofType="com.cg.springSecurity.pojo.Role">
<id column="rid" property="id"/>
<result column="name" property="name"/>
<result column="nameZh" property="nameZh"/>
</collection>
</resultMap>
<select id="getAllMenu" resultMap="MenuInfoMap">
SELECT m.id AS mid,m.pattern,r.id AS rid ,r.name,r.nameZh
FROM security.menu m , security.role r ,security.menu_role mr
WHERE m.`id`=mr.`mid` AND mr.`rid` = r.`id`;
</select>
</mapper>
1.4 修改security相关类
package com.cg.springSecurity.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.cg.springSecurity.mapper.UserMapper;
import com.cg.springSecurity.pojo.User;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws BadCredentialsException {
//调用usersMapper方法,根据用户名查询数据库
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
User user = userMapper.selectOne(wrapper);
user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
user.setRoles(userMapper.getUserRolesByRole(user.getRole()));
//手动设置了role,也可以通过数据库查询获取
// List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin"); //配置角色
return user;
}
}
添加自定义的路径和权限映射
package com.cg.springSecurity.security;
import com.cg.springSecurity.mapper.MenuMapper;
import com.cg.springSecurity.pojo.Menu;
import com.cg.springSecurity.pojo.Role;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
/**
* SecurityMetadataSource接口负责提供受保护对象所需要的权限。由于该案例中,受保护对象所需要的权限保存在数据库中,所以可以通过自定义类继承自
* FilterInvocationSecurityMetadataSource,并重写getAttributes方法来提供受保护对象所需要的权限。
*/
@Component
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Resource
MenuMapper menuMapper;
AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 在基于URL地址的权限控制中,受保护对象就是FilterInvocation。
* @param object 受保护对象
* @return 受保护对象所需要的权限
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 从受保护对象FilterInvocation中提取出当前请求的URI地址,例如/admin/hello
String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
// 查询所有菜单数据(每条数据中都包含访问该条记录所需要的权限)
List<Menu> allMenu = menuMapper.getAllMenu();
// 遍历菜单数据,如果当前请求的URL地址和菜单中某一条记录的pattern属性匹配上了(例如/admin/hello匹配上/admin/**)
// 那么就可以获取当前请求所需要的权限;如果没有匹配上,则返回null。需要注意的是,如果AbstractSecurityInterceptor
// 中的rejectPublicInvocations属性为false(默认值)时,则表示当getAttributes返回null时,允许访问受保护对象
for (Menu menu : allMenu) {
if (antPathMatcher.match(menu.getPattern(), requestURI)) {
String[] roles = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);
return SecurityConfig.createList(roles);
}
}
return null;
}
/**
* 方便在项目启动阶段做校验,如果不需要校验,则直接返回null即可。
* @return 所有的权限属性
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* 表示当前对象支持处理的受保护对象是FilterInvocation。
*/
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
在配置文件中添加路径权限映射处理器
//添加自定义权限管理器
ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
// 使用配置好的CustomSecurityMetadataSource来代替默认的SecurityMetadataSource对象
object.setSecurityMetadataSource(customSecurityMetadataSource);
// 将rejectPublicInvocations设置为true,表示当getAttributes返回null时,不允许访问受保护对象
object.setRejectPublicInvocations(false);
return object;
}
});
1.5 自定义权限不足的异常处理器
package com.cg.springSecurity.security;
import com.cg.springSecurity.base.ResultInfo;
import com.google.gson.Gson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Autowired
private Gson gson;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//设置响应的类型和编码
response.setHeader("Content-Type", "application/json;charset=utf-8");
response.getWriter().write(gson.toJson(new ResultInfo<>(403, "error!", "权限不足")));
}
}
http.formLogin() //自定义自己编写的登陆页面
// .loginPage("/login.html") //登陆页面设置
.loginProcessingUrl("/user/login") //登陆访问路径
.successHandler(authenticationSuccessHandler)
// .defaultSuccessUrl("/success" , true) //登陆成功后跳转路径
// .successForwardUrl("/")
// .failureUrl("/user/fail")//登录失败跳转的路径
.failureHandler(authenticationFailureHandler)
.permitAll()
.and().exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint)//登陆异常处理
1.6 postman测试
添加测试controller
package com.cg.springSecurity.controller;
import com.cg.springSecurity.base.ResultInfo;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author cg
* @date 2023/3/29 9:56
*/
@RestController
@CrossOrigin
public class IndexController {
@GetMapping("/admin")
public ResultInfo admin() {
return new ResultInfo("这是管理员。。。。");
}
@GetMapping("/user1")
public ResultInfo user() {
return new ResultInfo("这是用户。。。。");
}
@GetMapping("/visitor")
public ResultInfo visitor() {
return new ResultInfo("这是游客。。。。");
}
@GetMapping("/other")
public ResultInfo other() {
return new ResultInfo("这是其他。。。。");
}
}
登录游客
可以访问游客
无法访问/user1
可以访问未添加到url管理中的路径
由这一句代码来控制
// 将rejectPublicInvocations设置为true,表示当getAttributes返回null时,不允许访问受保护对象
object.setRejectPublicInvocations(false);
2.基于方法的权限管理
耳熟能详的AOP,先在security的配置文件上开启该功能@EnableGlobalMethodSecurity,然后直接在方法上添加注释即可。这种方法对代码没用侵入性,这里就不做介绍了,有兴趣直接参考开头大佬的文章。