springsecurity认证案例实现
本篇主要写一个实现案例
不说别的首先建表,下面是本案例需要的几张表,分别是用户表(user),角色表(role),菜单表(menu,这里定义了资源路径串),还有就是连接这三张表关系的用户角色表(user_role),然后菜单角色(menu_role)。
create database auth_security;
use auth_security;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- 创建菜单资源表
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pattern` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 给菜单资源表插入访问路径串
BEGIN;
INSERT INTO `menu` VALUES (1, '/admin/**');
INSERT INTO `menu` VALUES (2, '/user/**');
INSERT INTO `menu` VALUES (3, '/guest/**');
COMMIT;
-- 创建菜单和角色的关联表(中间表)
DROP TABLE IF EXISTS `menu_role`;
CREATE TABLE `menu_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`mid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `mid` (`mid`),
KEY `rid` (`rid`),
CONSTRAINT `menu_role_ibfk_1` FOREIGN KEY (`mid`) REFERENCES `menu` (`id`),
CONSTRAINT `menu_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- 插入值
BEGIN;
INSERT INTO `menu_role` VALUES (1, 1, 1);
INSERT INTO `menu_role` VALUES (2, 2, 2);
INSERT INTO `menu_role` VALUES (3, 3, 3);
INSERT INTO `menu_role` VALUES (4, 3, 2);
COMMIT;
-- 创建角色表
DROP TABLE IF EXISTS `role`;
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 AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 插入值
BEGIN;
INSERT INTO `role` VALUES (1, 'ROLE_ADMIN', '系统管理员');
INSERT INTO `role` VALUES (2, 'ROLE_USER', '普通用户');
INSERT INTO `role` VALUES (3, 'ROLE_GUEST', '游客');
COMMIT;
-- 创建用户表
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL,
`locked` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 插入值
BEGIN;
INSERT INTO `user` VALUES (1, 'admin', '{noop}123', 1, 0);
INSERT INTO `user` VALUES (2, 'user', '{noop}123', 1, 0);
INSERT INTO `user` VALUES (3, 'blr', '{noop}123', 1, 0);
COMMIT;
-- 用户角色关联表
use auth_security;
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uid` (`uid`),
KEY `rid` (`rid`),
CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`),
CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
BEGIN;INSERT INTO `user_role` VALUES (1, 1, 1);INSERT INTO `user_role` VALUES (2, 1, 2);INSERT INTO `user_role` VALUES (3, 2, 2);INSERT INTO `user_role` VALUES (4, 3, 3);COMMIT;SET FOREIGN_KEY_CHECKS = 1;
表结构简历完成后可以清楚的看到这几张表之间的关系,逆向表关系模型。
在idea中我们做一些配置
首先将实体类定义出来,这里为了简单,就定义了三个实体类,我们让User实体类直接去实现UserDetails。不管你新拓展一个DTO还是直接用我们这个实体类去实现这个UserDetails,我们需要注意自定义的话,我们需要去给它实现完成这个角色方法。role肯定是一个列表啦,因为每个用户可能有多个角色。
public class User implements UserDetails {
private Integer id;
private String password;
private String username;
private boolean enabled;
private boolean locked;
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList());
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !locked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
public void setId(Integer id) {
this.id = id;
}
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setLocked(boolean locked) {
this.locked = locked;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
public Integer getId() {
return id;
}
public List<Role> getRoles() {
return roles;
}
}
然后就是角色实体类
package com.jgdabc.entity;
public class Role {
private Integer id;
private String name;
private String nameZh;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNameZh() {
return nameZh;
}
public void setNameZh(String nameZh) {
this.nameZh = nameZh;
}
}
然后就是菜单实体类,这个菜单实体类也需要去对应到角色列表。这个实体类对应我们的授权至关重要。
package com.jgdabc.entity;
import java.util.List;
public class Menu {
private Integer id;
private String pattern;
private List<Role> roles;
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getPattern() {
return pattern;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
}
我们自定义数据源,这个数据源将来要配置到配置中,取代默认的内存数据源。
@Service
public class UserService implements UserDetailsService {
private final UserMapper userMapper;
@Autowired
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
user.setRoles(userMapper.getUserRoleByUid(user.getId()));
return user;
}
}
注意我们数据源中的方法,这个我们要在mybatis写sql的。其实这两个方法都是帮助认证的。
@Mapper
public interface UserMapper {
// 根据用户id获取角色信息
List<Role> getUserRoleByUid(Integer uid);
// 根据用户名获取用户
User loadUserByUsername(String username);
}
展示mapper.xml
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jgdabc.dao.UserMapper">
<select id="loadUserByUsername" resultType="com.jgdabc.entity.User">
select *
from user
where username = #{username};
</select>
<select id="getUserRoleByUid" resultType="com.jgdabc.entity.Role">
select r.*
from role r,
user_role ur
where ur.uid = #{uid}
and ur.rid = r.id
</select>
</mapper>
要想实现授权,我们需要自定义 自定义动态资源权限元数据信息,我们需要去写这样一个类。
这里的自定义元数据所做的就是如果我们通过获取请求的路径然后与遍历的菜单资源做匹配,如果匹配了,然后url匹配到的菜单所需要的所有角色加入到Security中。这样可以确定某个路径是什么样的角色可以访问。
package com.jgdabc.config;
import com.jgdabc.entity.Menu;
import com.jgdabc.service.MenuService;
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 java.util.Collection;
import java.util.List;
/**
* @author 兰舟千帆
* @version 1.0
* @date 2023/7/10 16:26
* @Description 功能描述:
*/
@Component
public class CustomSecurityMeataSource implements FilterInvocationSecurityMetadataSource {
private final MenuService menuService;
public CustomSecurityMeataSource(MenuService menuService) {
this.menuService = menuService;
}
AntPathMatcher antPathMatcher = new AntPathMatcher();
// 自定义动态资源权限元数据信息
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 获取当前的请求对象
String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
List<Menu> allMenu = menuService.getAllMenu();
for (Menu menu : allMenu) {
if (antPathMatcher.match(menu.getPattern(), requestURI)) {
String[] roles = menu.getRoles().stream().map(r -> r.getName()).toArray(String[]::new);
return SecurityConfig.createList(roles);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
配置
package com.jgdabc.config;
import com.jgdabc.entity.Menu;
import com.jgdabc.service.MenuService;
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 java.util.Collection;
import java.util.List;
/**
* @author 兰舟千帆
* @version 1.0
* @date 2023/7/10 16:26
* @Description 功能描述:
*/
@Component
public class CustomSecurityMeataSource implements FilterInvocationSecurityMetadataSource {
private final MenuService menuService;
public CustomSecurityMeataSource(MenuService menuService) {
this.menuService = menuService;
}
AntPathMatcher antPathMatcher = new AntPathMatcher();
// 自定义动态资源权限元数据信息
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 获取当前的请求对象
String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
List<Menu> allMenu = menuService.getAllMenu();
// /遍历资源,查找用户访问的资源
for (Menu menu : allMenu) {
//判断请求url与菜单角色是否匹配
if (antPathMatcher.match(menu.getPattern(), requestURI)) {
//将url匹配到的菜单所需要的所有角色加入到Security中
String[] roles = menu.getRoles().stream().map(r -> r.getName()).toArray(String[]::new);
return SecurityConfig.createList(roles);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
controller
package com.jgdabc.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/admin/hello")
public String admin() {
return "hello admin";
}
@GetMapping("/user/hello")
public String user() {
return "hello user";
}
@GetMapping("/guest/hello")
public String guest() {
return "hello guest";
}
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
设计出来我们就可以去做测试了。这是一个简单的案例。