赞
踩
注:本文基于
Spring Boot 3.2.1
以及Spring Security 6.2.1
【SpringBoot3】Spring Security 核心概念
【SpringBoot3】Spring Security 常用注解
【SpringBoot3】Spring Security 详细使用实例(简单使用、JWT模式)
1)创建相关数据库表
2)从数据库中读取用户、角色、权限数据
3)登录成功返回token
4)访问相关资源时,通过自定义的过滤器解析token,判断权限
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.21</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
1)增加数据库链接配置
# 允许循环引用 spring.main.allow-circular-references=true # --- 数据库链接配置 spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.type=com.alibaba.druid.pool.DruidDataSource # --- mybatis-plus start # 关闭MP3.0自带的banner mybatis-plus.global-config.banner=false # 主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)"; mybatis-plus.global-config.db-config.id-type=ASSIGN_ID # 返回类型为Map,显示null对应的字段 mybatis-plus.configuration.call-setters-on-nulls=true # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
2)创建数据库
创建5张表:system_menu、system_role、system_role_menu、system_user_role、system_users
这些表可作为前后端分离项目的通用权限表(如果是简单演示2张表就可以了)
CREATE TABLE `system_role` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID', `name` varchar(30) NOT NULL COMMENT '角色名称', `code` varchar(100) NOT NULL COMMENT '角色权限字符串', `sort` int NOT NULL COMMENT '显示顺序', `status` tinyint NOT NULL COMMENT '角色状态(0正常 1停用)', `remark` varchar(500) DEFAULT NULL COMMENT '备注', `creator` varchar(64) DEFAULT '' COMMENT '创建者', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updater` varchar(64) DEFAULT '' COMMENT '更新者', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=140 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色信息表'; CREATE TABLE `system_menu` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID', `name` varchar(50) NOT NULL COMMENT '菜单名称', `permission` varchar(100) NOT NULL DEFAULT '' COMMENT '权限标识', `type` tinyint NOT NULL COMMENT '菜单类型(1目录 2菜单 3按钮)', `sort` int NOT NULL DEFAULT '0' COMMENT '显示顺序', `parent_id` bigint NOT NULL DEFAULT '0' COMMENT '父菜单ID', `path` varchar(200) DEFAULT '' COMMENT '路由地址', `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标', `component` varchar(255) DEFAULT NULL COMMENT '组件路径', `component_name` varchar(255) DEFAULT NULL COMMENT '组件名', `status` tinyint NOT NULL DEFAULT '0' COMMENT '菜单状态', `visible` bit(1) NOT NULL DEFAULT b'1' COMMENT '是否可见', `keep_alive` bit(1) NOT NULL DEFAULT b'1' COMMENT '是否缓存', `always_show` bit(1) NOT NULL DEFAULT b'1' COMMENT '是否总是显示', `creator` varchar(64) DEFAULT '' COMMENT '创建者', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updater` varchar(64) DEFAULT '' COMMENT '更新者', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB COMMENT='菜单权限表'; CREATE TABLE `system_role_menu` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增编号', `role_id` bigint NOT NULL COMMENT '角色ID', `menu_id` bigint NOT NULL COMMENT '菜单ID', `creator` varchar(64) DEFAULT '' COMMENT '创建者', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updater` varchar(64) DEFAULT '' COMMENT '更新者', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB COMMENT='角色和菜单关联表'; CREATE TABLE `system_users` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(30) NOT NULL COMMENT '用户账号', `password` varchar(100) NOT NULL DEFAULT '' COMMENT '密码', `nickname` varchar(30) NOT NULL COMMENT '用户昵称', `remark` varchar(500) DEFAULT NULL COMMENT '备注', `email` varchar(50) DEFAULT '' COMMENT '用户邮箱', `mobile` varchar(11) DEFAULT '' COMMENT '手机号码', `sex` tinyint DEFAULT '0' COMMENT '用户性别', `avatar` varchar(512) DEFAULT '' COMMENT '头像地址', `status` tinyint NOT NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)', `login_ip` varchar(50) DEFAULT '' COMMENT '最后登录IP', `login_date` datetime DEFAULT NULL COMMENT '最后登录时间', `creator` varchar(64) DEFAULT '' COMMENT '创建者', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updater` varchar(64) DEFAULT '' COMMENT '更新者', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `idx_username` (`username`,`update_time`) USING BTREE ) ENGINE=InnoDB COMMENT='用户信息表'; CREATE TABLE `system_user_role` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增编号', `user_id` bigint NOT NULL COMMENT '用户ID', `role_id` bigint NOT NULL COMMENT '角色ID', `creator` varchar(64) DEFAULT '' COMMENT '创建者', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updater` varchar(64) DEFAULT '' COMMENT '更新者', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB COMMENT='用户和角色关联表';
springsecurity框架提供了示例的两张表,在文件
users.ddl
中
create table users(username varchar(50) not null primary key,password varchar(500) not null,enabled boolean not null);
create table authorities (username varchar(50) not null,authority varchar(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
@Mapper public interface MenuMapper extends BaseMapper<MenuDO> { } @Mapper public interface RoleMapper extends BaseMapper<RoleDO> { } @Mapper public interface RoleMenuMapper extends BaseMapper<RoleMenuDO> { default List<RoleMenuDO> selectListByRoleId(Collection<Long> roleIds) { return selectList(new LambdaQueryWrapper<RoleMenuDO>().eq(RoleMenuDO::getRoleId, roleIds)); } } @Mapper public interface UserMapper extends BaseMapper<UserDO> { default UserDO selectByUsername(String username) { return selectOne(new LambdaQueryWrapper<UserDO>().eq(UserDO::getUsername, username)); } } @Mapper public interface UserRoleMapper extends BaseMapper<UserRoleDO> { default List<UserRoleDO> selectListByUserId(Long userId) { return selectList(new LambdaQueryWrapper<UserRoleDO>().eq(UserRoleDO::getUserId, userId)); } }
注意: SecurityExpressionRoot#hasAnyRole
方法固定加上前缀ROLE_
,因此,如果数据库的roleCode没有前缀ROLE_
,则需在查询的时候拼接上去。
spring security的角色和权限是一个概念,只是字符串不一样,角色必须加前缀ROLE_
,权限没有这要求。
@Service @Slf4j public class UserServiceImpl implements UserService { @Autowired private RoleService roleService; @Autowired private PermissionService permissionService; @Autowired private MenuService menuService; @Resource private UserMapper userMapper; /** * 返回`UserDetails`实现类`UserEntity` */ @Override public UserEntity loadUserByUsername(String username) { UserEntity entity = new UserEntity(); UserDO user = getUserByUsername(username); Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(user.getId()); List<RoleDO> roles = roleService.getRoleList(roleIds); Set<Long> menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId)); List<MenuDO> menuList = menuService.getMenuList(menuIds); entity.setId(user.getId()); entity.setUsername(user.getUsername()); entity.setPassword(user.getPassword()); // 注意:SecurityExpressionRoot#hasAnyRole方法固定加上前缀ROLE_, // 因此,如果数据库的roleCode没有前缀ROLE_,则需在查询的时候拼接上去 Set<String> roleCodes = convertSet(roles, role -> "ROLE_" + role.getCode()); Set<String> permCodes = convertSet(menuList, MenuDO::getPermission); roleCodes.addAll(permCodes); List<String> authorities = roleCodes.stream().filter(StrUtil::isNotBlank).toList(); // spring security的角色和权限是一个概念,只是字符串不一样,角色必须加前缀ROLE_,权限没有这要求 entity.setAuthorities(AuthorityUtils.createAuthorityList(authorities)); return entity; } @Override public UserDO getUserByUsername(String username) { return userMapper.selectByUsername(username); } } /** * 权限 Service 实现类 */ @Service @Slf4j public class PermissionServiceImpl implements PermissionService { @Resource private RoleMenuMapper roleMenuMapper; @Resource private UserRoleMapper userRoleMapper; @Resource private RoleService roleService; @Resource private MenuService menuService; @Override public Set<Long> getRoleMenuListByRoleId(Collection<Long> roleIds) { if (CollUtil.isEmpty(roleIds)) { return Collections.emptySet(); } // 如果是管理员的情况下,获取全部菜单编号 if (roleService.hasAnySuperAdmin(roleIds)) { return convertSet(menuService.getMenuList(), MenuDO::getId); } // 如果是非管理员的情况下,获得拥有的菜单编号 return convertSet(roleMenuMapper.selectListByRoleId(roleIds), RoleMenuDO::getMenuId); } @Override public Set<Long> getUserRoleIdListByUserId(Long userId) { return convertSet(userRoleMapper.selectListByUserId(userId), UserRoleDO::getRoleId); } } /** * 角色 Service 实现类 */ @Service @Slf4j public class RoleServiceImpl implements RoleService { @Resource private RoleMapper roleMapper; @Override public RoleDO getRole(Long id) { return roleMapper.selectById(id); } @Override public List<RoleDO> getRoleList(Collection<Long> ids) { if (CollectionUtil.isEmpty(ids)) { return Collections.emptyList(); } return roleMapper.selectBatchIds(ids); } @Override public boolean hasAnySuperAdmin(Collection<Long> ids) { if (CollectionUtil.isEmpty(ids)) { return false; } return ids.stream().anyMatch(id -> { RoleDO role = getRole(id); return role != null && RoleCodeEnum.isSuperAdmin(role.getCode()); }); } } /** * 菜单 Service 实现 */ @Service @Slf4j public class MenuServiceImpl implements MenuService { @Resource private MenuMapper menuMapper; @Override public List<MenuDO> getMenuList() { return menuMapper.selectList(new LambdaQueryWrapper<>()); } @Override public List<MenuDO> getMenuList(Collection<Long> ids) { return menuMapper.selectBatchIds(ids); } }
UserDetailsService
实现类创建 MyUserDetailsServiceImpl 实现Spring Security接口 UserDetailsService
调用userService
方法,返回UserDetails
实现类UserEntity
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userService.loadUserByUsername(username);
}
}
JwtTokenFilter
@Component public class JwtTokenFilter extends OncePerRequestFilter { @Autowired UserService userService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 验证token是否有效 String token = request.getHeader("token"); if (StrUtil.isNotEmpty(token)) { String secret = "0123456789"; boolean verify = JWTUtil.verify(token, secret.getBytes()); if (!verify) { response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\":401,\"msg\":\"token无效\"}"); return; } else { //认证成功,设置用户信息 UserEntity user = JWTUtil.parseToken(token).getPayloads().toBean(UserEntity.class); // 模拟获取用户信息,实际情况应该是从数据库查询 UserDetails userDetails = userService.loadUserByUsername(user.getUsername()); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } filterChain.doFilter(request, response); } }
HttpSecurity
1)使用自定义的MyUserDetailsServiceImpl
2)使用自定义过滤器JwtTokenFilter
3)登录成功,根据UserEntity生成token,返回给前端。
4)基于 token 机制,所以不需要 Session
@EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) @Configuration @Slf4j public class JwtSecurityConfig { @Autowired private MyUserDetailsServiceImpl userDetailService; @Autowired private JwtTokenFilter jwtTokenFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .requestMatchers("/login").permitAll() .anyRequest().authenticated() ) .userDetailsService(userDetailService) .exceptionHandling(ex -> ex.authenticationEntryPoint((request, response, authException) -> { log.error("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), authException); ServletUtils.writeJSON(response, authException.getMessage()); }).accessDeniedHandler((request, response, accessDeniedException) -> { log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), "", accessDeniedException); ServletUtils.writeJSON(response, accessDeniedException.getMessage()); })) .logout(logout -> logout.invalidateHttpSession(true)) // 配置登录页面 .httpBasic(Customizer.withDefaults()) .formLogin(form -> form.loginPage("/login").permitAll() .loginProcessingUrl("/login") .successHandler((request, response, authentication) -> { // 登录成功,根据UserEntity生成token,返回给前端 log.info("登录成功:{}", authentication); UserEntity principal = (UserEntity) authentication.getPrincipal(); String secret = "0123456789"; Map<String, Object> payload = new HashMap<>(); payload.put("id", principal.getId()); payload.put("username", principal.getUsername()); String token = JWTUtil.createToken(payload, secret.getBytes()); response.setContentType("application/json;charset=UTF-8"); Map<String, Object> map = new HashMap<>(); map.put("token", token); response.getWriter().write(JSONUtil.toJsonStr(map)); }).failureHandler((request, response, authentication) -> { log.info("登录失败", authentication); })); // CSRF 禁用,因为不使用 Session http.csrf(AbstractHttpConfigurer::disable); // 基于 token 机制,所以不需要 Session http.sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } }
1、使用postman请求登录接口,密码验证成功后,通过 successHandler
返回token
2、将token当作Header参数,请求 /admin
等需要权限的接口地址
3、 定义过滤器JwtTokenFilter
接收到请求,解析Token校验账户权限,设置当前登录用户信息
4、权限校验完成!
Spring Security 提供了拦截器,用于控制对安全对象的访问,这些安全对象可以是方法调用或 Web 请求。在调用之前,AuthorizationManager
实例会作出决策,判断调用是否允许继续。此外,AuthorizationManager
实例还会在调用之后作出决策,判断给定的值是否可以返回。
动态权限可以通过创建自定义类(如 MyAuthorizationManager
)实现接口 AuthorizationManager
,在实现类的 check()
方法中判断用户是否有请求资源的权限。
通常是结合数据库中配置的数据判断当前用户是否有当前请求
URI
的权限
MyAuthorizationManager,实现接口 AuthorizationManager
@Component @Slf4j public class MyAuthorizationManager implements AuthorizationManager<HttpServletRequest> { @Autowired private PermissionService permissionService; @Override public AuthorizationDecision check(Supplier<Authentication> authentication, HttpServletRequest request) { log.info("Checking authorization {}", request); String requestURI = request.getRequestURI(); //根据这些信息 和业务写逻辑即可 最终决定是否授权 isGrantedboolean isGranted = true; boolean isGranted = true; if (requestURI.equals("/login") || requestURI.equals("/logout")) { return new AuthorizationDecision(isGranted); } //当前用户的权限信息 比如角色 Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities(); // 当前用户权限代码 List<String> userPermCodes = authorities.stream().map(GrantedAuthority::getAuthority).toList(); // 查询当前被访问的URL:需要哪些权限才能访问 List<String> codes = permissionService.getPermissions(requestURI); if(codes == null || codes.isEmpty() ){ return new AuthorizationDecision(true); } // 如果包含用户所拥有的权限,则授权通过,否则授权失败 boolean containsed = CollectionUtil.containsAny(codes, userPermCodes); if(!containsed){ isGranted = false; } return new AuthorizationDecision(isGranted); } }
添加过滤器 AuthorizationFilter
,传入自定义权限处理类 MyAuthorizationManager
@EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) @Configuration @Slf4j public class JwtSecurityConfig { @Autowired private MyAuthorizationManager authorizationManager; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // -- 省略其他代码 -- // http.addFilter(new AuthorizationFilter(authorizationManager)); return http.build(); } }
通过以上配置,每次接口请求,都会经过MyAuthorizationManager
,用户可以自行判断是否通过授权。
通过返回值 new AuthorizationDecision(isGranted)
,决定是否授权成功!
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。