Article directory
1. RBAC permission model
1. Model introduction
RBAC (Role Based Access Control) Chinese full name is role-based access control. In the RBAC model, permissions are associated with roles, and different roles have different permissions. Users can obtain permissions of different roles by being assigned to different roles, thereby simplifying user permission management. After the user is associated with the role, they can also perform independent authorization and privilege franchise, and the authorization information must be controlled through the role to achieve access control.
2. Create a database table based on the model
Create a simple database based on the RBAC model, the integrity constraints are not too strong, and the functions are simply implemented.
-- ----------------------------
-- Table structure for menu
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '权限id',
`menu_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限名',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES (1, 'sys:user:insert');
INSERT INTO `menu` VALUES (2, 'sys:user:delete');
INSERT INTO `menu` VALUES (3, 'sys:student:insert');
INSERT INTO `menu` VALUES (4, 'sys:student:delete');
INSERT INTO `menu` VALUES (5, 'sys:studet:update');
INSERT INTO `menu` VALUES (6, 'sys:user:login');
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色id',
`role_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色名',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'admin');
INSERT INTO `role` VALUES (2, 'student');
INSERT INTO `role` VALUES (3, 'teacher');
-- ----------------------------
-- Table structure for role_menu
-- ----------------------------
DROP TABLE IF EXISTS `role_menu`;
CREATE TABLE `role_menu` (
`role_id` bigint NOT NULL COMMENT '角色id',
`menu_id` bigint NOT NULL COMMENT '权限id',
PRIMARY KEY (`role_id`, `menu_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role_menu
-- ----------------------------
INSERT INTO `role_menu` VALUES (1, 1);
INSERT INTO `role_menu` VALUES (1, 2);
INSERT INTO `role_menu` VALUES (1, 3);
INSERT INTO `role_menu` VALUES (1, 4);
INSERT INTO `role_menu` VALUES (1, 5);
INSERT INTO `role_menu` VALUES (1, 6);
INSERT INTO `role_menu` VALUES (2, 1);
INSERT INTO `role_menu` VALUES (2, 3);
INSERT INTO `role_menu` VALUES (2, 6);
INSERT INTO `role_menu` VALUES (3, 1);
INSERT INTO `role_menu` VALUES (3, 3);
INSERT INTO `role_menu` VALUES (3, 4);
INSERT INTO `role_menu` VALUES (3, 6);
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户id',
`user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'cx', '$10$S47LvooOhnDT0sVZ7DlShuqfEqcwhyhU3F9GKt0mbgx1vym.zZXWS');
INSERT INTO `user` VALUES (2, 'cxx', '$10$S47LvooOhnDT0sVZ7DlShuqfEqcwhyhU3F9GKt0mbgx1vym.zZXWS');
INSERT INTO `user` VALUES (3, 'cxxx', '$10$S47LvooOhnDT0sVZ7DlShuqfEqcwhyhU3F9GKt0mbgx1vym.zZXWS');
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_id` bigint NOT NULL COMMENT '用户ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
PRIMARY KEY (`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1);
INSERT INTO `user_role` VALUES (2, 2);
INSERT INTO `user_role` VALUES (3, 2);
INSERT INTO `user_role` VALUES (3, 3);
SET FOREIGN_KEY_CHECKS = 1;
2. Implementation of authorization
1. Query authorization information according to id
Query authorization information from the database based on user information. Define permission entity class, mapper interface, mapper mapping file.
Entity class:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Menu implements Serializable {
private static final long serialVersionUID = -54979041104113736L;
@TableId
private Long id;
/**
* 菜单名
*/
private String menuName;
}
mapper interface:
@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
List<String> selectMenusByUserId(@Param("id") Long id);
}
mapper mapping file: Since it is only a simple test, multi-table query is used, regardless of performance.
<?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.cx.authorities.mapper.MenuMapper">
<select id="selectMenusByUserId" resultType="java.lang.String">
select
DISTINCT
menu.menu_name
from
user_role,role,role_menu,menu
where
user_id = ${id} and
user_role.role_id = role.id and
role.id = role_menu.role_id and
role_menu.menu_id = menu.id
</select>
</mapper>
2. Package authorization information
The method will be overridden in UserDetails
the implementation class getAuthorities()
, and the return value object is the set of permissions owned by the user. First, you need to query the corresponding permissions under the corresponding role of the user in the database, and pass it into the UserDetailsService
login user entity class LoginUser
. Since security needs GrantedAuthority
type permission information, it is necessary to LoginUser
replace all incoming permission information with GrantedAuthority
type. SimpleGrantedAuthority
It is GrantedAuthority
an implementation class that accepts string information, so it directly converts string permissions into SimpleGrantedAuthority
.
/**
* 实现UserDetails接口
*/
@Data
@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;
}
//存储SpringSecurity所需要的权限信息的集合
@JSONField(serialize = false)
private List<GrantedAuthority> authorities;
/**
* 返回权限信息
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
authorities = new ArrayList<>();
//把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
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();
}
/**
* 判断用户是否没过期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 判断用户是否锁定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 判断用户是否没有超时
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 判断用户是否可用
* @return
*/
@Override
public boolean isEnabled() {
return true;
}
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
// 查询用户信息
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName, userName);
User user = userMapper.selectOne(wrapper);
// 未查询到用户
if(Objects.isNull(user)){
throw new RuntimeException("用户名不存在");
}
// 查询用户权限
List<String> permissionKeyList = menuMapper.selectMenusByUserId(user.getId());
// 查询成功返回用户信息
return new LoginUser(user,permissionKeyList);
}
}
3. Store permission information in SecurityContextHolder
After the user information is encapsulated into a type in UserDetails
the implementation class method , the security needs to encapsulate the user information and its permission information into an object and pass it in .LoginUser
SimpleGrantedAuthority
authenticationToken
SecurityContextHolder
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = httpServletRequest.getHeader("token");
if (!StringUtils.hasText(token)) {
// 放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
// 解析token
String id;
try {
Claims claims = JwtUtil.parseJWT(token);
id = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
// redis获取用户信息
String redisKey = "login:" + id;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
// 存入securityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
4. Open permission configuration
After the permission information is encapsulated and passed in SecurityContextHolder
, permission scanning needs to be enabled on the configuration class. Annotate directly on the configuration class@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
5. Configure interface access rights
Finally, configure the required permissions on the API interface that requires authorization.
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping
@PreAuthorize("hasAuthority('sys:user:insert')")
public String hello(){
return "hello";
}
}
3. Custom exception handler
1. Render response tool class
/**
* 渲染响应的工具类
*/
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;
}
}
2. Authentication exception handler
/**
* 认证异常处理器
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
R result = R.failed("认证失败");
String json = JSON.toJSONString(result);
WebUtils.renderString(response,json);
}
}
3. Authorization exception handler
/**
* 自定义授权异常
*/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
R result = R.failed("授权失败");
String json = JSON.toJSONString(result);
WebUtils.renderString(response,json);
}
}
4. Configure exception handler
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
@Autowired
private AuthenticationEntryPointImpl authenticationEntryPoint;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 添加jwt认证过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加异常处理器
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
}
}
4. The effect of authorization
1. Authentication success and failure
Authentication succeeded:
Authentication failed:
2. Authorization success and failure
Authorization succeeded:
Can access the interface and accept data.
Authorization failed:
5. Cross-domain configuration
1. springBoot configuration
@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);
}
}
2. Spring Security configuration
//允许跨域
http.cors();