SpringSecurity系列——登录流程修改day5-1(源于官网5.7.2版本)
前言
技术 | 版本 |
---|---|
jdk | 17 |
SpringSecurity | 5.7.2 |
SpringBoot | 2.7.2 |
修改登录用户名密码
注意点
首先我们要知道,在新的SpringSecurity中我们不需要继承WebSecurityConfigurerAdapter,之前在修改登录的用户名密码我们知道应该去修改AuthenticationManager,进行覆盖或重写,但也可以直接已注入Bean的形式改写UserDetailsService如下:
采用基于内存的实现方式
package com.example.test1.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
public class SpringSecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("user").password("{noop}12345").roles("admin").build());
return inMemoryUserDetailsManager;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.userDetailsService(userDetailsService());
return http.build();
}
}
如此一来我们就可以使用自定义的用户名密码登录了
登录用户名:user
密码:12345
在后续新特性中这种方式依然可以使用
新特性更新讲解(这里我不是很确定,因为我上手的时候就已经是新特性了)
在这里大家注意一个地方就是
我们来看一下原始的
可以发现原始的是重写了方法达到覆盖的效果,直接在HttpSecurity中可以直接设置重写,使得配置更加简单
自定义数据源的认证
解读UserDetails
层次结构
源码解读
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
在UserDetsils接口中规范的基本属性:
- 用户权限列表:
Collection<? extends GrantedAuthority> getAuthorities();
- 用户名:
String getUsername();
- 密码:
String getPassword();
- 账户是否过期:
boolean isAccountNonExpired();
- 账户是否被锁定:
boolean isAccountNonLocked();
- 凭证是否过期:
boolean isCredentialsNonExpired();
- 是否开启使用:
boolean isEnabled();
解读User
实现了UserDetails接口
所属包
package org.springframework.security.core.userdetails;
属性
private static final long serialVersionUID = 570L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
除了在UserDetails的属性
- serialVersionUID:版本UUID
- logger:日志信息
注意password我们要指定加密方式
这也就提醒我们数据库的设计方式
简单数据库设计
User表
authorities表
sql文件
-- ----------------------------
-- Table structure for authorities
-- ----------------------------
DROP TABLE IF EXISTS `authorities`;
CREATE TABLE `authorities` (
`userId` bigint(20) NOT NULL,
`authority` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限角色',
`expired` tinyint(4) NULL DEFAULT NULL COMMENT '账户过期',
`tokenExpired` tinyint(4) NULL DEFAULT NULL COMMENT '凭证过期',
`locaked` tinyint(4) NULL DEFAULT NULL COMMENT '账户锁定',
UNIQUE INDEX `ix_auth_username`(`userId`, `authority`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`userId` bigint(20) NOT NULL,
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`password` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`enabled` tinyint(1) NOT NULL COMMENT '0表示false,1表示true',
PRIMARY KEY (`userId`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
RBAC数据库设计
sql文件
/*
Navicat Premium Data Transfer
Source Server : mysql_syf
Source Server Type : MySQL
Source Server Version : 80017
Source Host : localhost:3306
Source Schema : spring_security_test
Target Server Type : MySQL
Target Server Version : 80017
File Encoding : 65001
Date: 24/07/2022 10:18:44
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for authorities
-- ----------------------------
DROP TABLE IF EXISTS `authorities`;
CREATE TABLE `authorities` (
`authorityId` bigint(20) NOT NULL,
`authority` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限角色',
`address` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`authorityId`) USING BTREE,
UNIQUE INDEX `ix_auth_username`(`authorityId`, `authority`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of authorities
-- ----------------------------
INSERT INTO `authorities` VALUES (1, '登录', '/login');
INSERT INTO `authorities` VALUES (2, '注册', '/register');
INSERT INTO `authorities` VALUES (3, '获取用户头像', '/getAvatar');
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`roleId` bigint(20) NOT NULL COMMENT '角色ID',
`roleName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色名',
`roleAnnotation` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '注释',
PRIMARY KEY (`roleId`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_guest', '游客');
INSERT INTO `role` VALUES (2, 'ROLE_admin', '管理员');
INSERT INTO `role` VALUES (3, 'ROLE_user', '普通用户');
-- ----------------------------
-- Table structure for role_authority_con
-- ----------------------------
DROP TABLE IF EXISTS `role_authority_con`;
CREATE TABLE `role_authority_con` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`authorityId` bigint(20) NULL DEFAULT NULL,
`roleId` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role_authority_con
-- ----------------------------
-- ----------------------------
-- Table structure for user_role_con
-- ----------------------------
DROP TABLE IF EXISTS `user_role_con`;
CREATE TABLE `user_role_con` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userId` bigint(20) NOT NULL,
`roleId` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role_con
-- ----------------------------
INSERT INTO `user_role_con` VALUES (1, 1, 2);
INSERT INTO `user_role_con` VALUES (2, 1, 3);
INSERT INTO `user_role_con` VALUES (3, 2, 1);
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`userId` bigint(20) NOT NULL,
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`password` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`enabled` tinyint(1) NOT NULL COMMENT '0表示false,1表示true',
`expired` tinyint(1) NULL DEFAULT NULL,
`tokenExpired` tinyint(1) NULL DEFAULT NULL,
`locked` tinyint(1) NULL DEFAULT NULL,
PRIMARY KEY (`userId`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES (1, 'user1', '{noop}123', 1, 1, 1, 1);
INSERT INTO `users` VALUES (2, 'user2', '{noop}456', 1, 1, 1, 1);
SET FOREIGN_KEY_CHECKS = 1;
1.编写entity实体类
1.1编写Role,UserRoleCon
Role
package com.example.test1.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "role")
public class Role {
@TableId(value = "roleId")
private long roleId;
@TableField(value = "roleName")
private String roleName;
@TableField(value = "roleAnnotation")
private String roleAnnotation;
}
UserRoleCon
package com.example.test1.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "user_role_con")
public class UserRoleCon {
private int id;
@TableField(value = "userId")
private long userId;
@TableField(value = "roleId")
private long roleId;
}
1.2User类实现UserDetails接口(重要)
我们的User实体类要直接实现UserDetails接口完成重写
重要部分
这一部分我们需要基于GrantedAuthority的实现SimpleGrantedAuthority构建出一个用户角色权限集合(可以选择Set、List)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
HashSet<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(x->{
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(x.getRoleName());
authorities.add(simpleGrantedAuthority);
});
return authorities;
}
package com.example.test1.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
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 org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "users")
public class Users implements UserDetails {
@TableId(value = "userId")
private long userId;
private String username;
private String password;
private boolean enabled;
private boolean expired;
@TableField(value = "tokenExpired")
private boolean tokenExpired;
private boolean locked;
@TableField(exist = false)
private ArrayList<Role> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
HashSet<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(x->{
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(x.getRoleName());
authorities.add(simpleGrantedAuthority);
});
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return expired;
}
@Override
public boolean isAccountNonLocked() {
return locked;
}
@Override
public boolean isCredentialsNonExpired() {
return tokenExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
public long getUserId() {
return userId;
}
public void setUserId(long userId) {
this.userId = userId;
}
public ArrayList<Role> getRoles() {
return roles;
}
public void setRoles(ArrayList<Role> roles) {
this.roles = roles;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setExpired(boolean expired) {
this.expired = expired;
}
public void setTokenExpired(boolean tokenExpired) {
this.tokenExpired = tokenExpired;
}
public void setLocked(boolean locked) {
this.locked = locked;
}
}
2.编写Mapper
UserMapper
package com.example.test1.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.test1.entity.Users;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<Users> {
}
RoleMapper
package com.example.test1.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.test1.entity.Role;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RoleMapper extends BaseMapper<Role> {
}
UserRoleConMapper
package com.example.test1.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.test1.entity.UserRoleCon;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserRoleConMapper extends BaseMapper<UserRoleCon> {
}
3.编写UserDetailsServiceImpl实现UserDetailsService接口
重要部分
重写loadUserByUsername方法,要求传入用户的用户名(这里的用户名即是认证表单传入的),通过用户名获取用户的信息,最终得到用户角色信息,即从UserDetailsService中查找UserDetails,用户名密码验证成功则返回UsernamePasswordAuthenticationToken
类型,其中就包含了UserDetails和Authorities(流程讲解)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//获取用户角色
Users user = this.defineLoadUserByUsername(username);
if (Objects.isNull(user)) {
throw new UsernameNotFoundException("用户名无法获取");
}
//获取权限信息
ArrayList<Role> roleArrayList = this.getRoleByUserId(user.getUserId());
user.setRoles(roleArrayList);
return user;
}
package com.example.test1.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.test1.entity.Role;
import com.example.test1.entity.UserRoleCon;
import com.example.test1.entity.Users;
import com.example.test1.mapper.RoleMapper;
import com.example.test1.mapper.UserMapper;
import com.example.test1.mapper.UserRoleConMapper;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
private final RoleMapper roleMapper;
private final UserRoleConMapper userRoleConMapper;
@Autowired
public UserDetailsServiceImpl(UserMapper userMapper, RoleMapper roleMapper, UserRoleConMapper userRoleConMapper) {
this.userMapper = userMapper;
this.userRoleConMapper = userRoleConMapper;
this.roleMapper = roleMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//获取用户角色
Users user = this.defineLoadUserByUsername(username);
if (Objects.isNull(user)) {
throw new UsernameNotFoundException("用户名无法获取");
}
//获取权限信息
ArrayList<Role> roleArrayList = this.getRoleByUserId(user.getUserId());
user.setRoles(roleArrayList);
return user;
}
public Users defineLoadUserByUsername(String username) {
LambdaQueryWrapper<Users> usersLambdaQueryWrapper = new LambdaQueryWrapper<>();
usersLambdaQueryWrapper.eq(Users::getUsername, username).last("limit 1");
return userMapper.selectOne(usersLambdaQueryWrapper);
}
public ArrayList<Role> getRoleByUserId(long userId) {
//获取roleId
LambdaQueryWrapper<UserRoleCon> userRoleConLambdaQueryWrapper = new LambdaQueryWrapper<>();
userRoleConLambdaQueryWrapper.eq(UserRoleCon::getUserId, userId).select(UserRoleCon::getRoleId);
List<UserRoleCon> userRoleCons = userRoleConMapper.selectList(userRoleConLambdaQueryWrapper);
ArrayList<Role> roleArrayList = new ArrayList<>();
for (UserRoleCon userRoleCon : userRoleCons) {
LambdaQueryWrapper<Role> roleLambdaQueryWrapper = new LambdaQueryWrapper<>();
roleLambdaQueryWrapper.eq(Role::getRoleId, userRoleCon.getRoleId()).select(Role::getRoleId, Role::getRoleName, Role::getRoleAnnotation).last("limit 1");
Role role = roleMapper.selectOne(roleLambdaQueryWrapper);
roleArrayList.add(role);
}
return roleArrayList;
}
}
4.编写SpringSecurityConfig
重要部分
//注入
private final UserDetailsServiceImpl userDetailsService;
@Autowired
public SpringSecurityConfig(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
//使用
http.userDetailsService(userDetailsService);
package com.example.test1.config;
import com.example.test1.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
@Configuration
public class SpringSecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
@Autowired
public SpringSecurityConfig(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
AuthenticationManager authenticationManager = configuration.getAuthenticationManager();
System.out.println(authenticationManager);
return configuration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.userDetailsService(userDetailsService);
return http.build();
}
}
5.编写Controller进行测试
package com.example.test1.controller;
import com.example.test1.param.LoginParam;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
@GetMapping("/index")
public String test(){
return "index....";
}
}
输入数据库中的用户名密码
认证错误注意点
- 我们需要对Role、User、UserRoleCon三张表进行填充
- 需要将User表中的
enabled,expired,tokenExpired,locked字段
填充为1,表示true(这里大家直接构建的时候要注意,我提供的sql文件是已经填充了数据的)
关于SpringBoot2.7+/Spring5.3+不建议字段注入问题
字段注入问题:
- 对象的外部可见性
- 可能导致循环依赖
- 无法设置注入的对象为final,也无法注入静态变量
不建议字段注入
@Autowired
private UserDetailsServiceImpl userDetailsService;
建议构造注入
private final UserDetailsServiceImpl userDetailsService;
@Autowired
public SpringSecurityConfig(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}