之前我们在测试的时候,都是使用的字符串充当用户名称和密码,本篇将其换成MySQL数据库.
一、替换为真实的MySQL
1.1 引入依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
1.2 创建表语句
DROP TABLE IF EXISTS `tb_sys_user`;
CREATE TABLE `tb_sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
`nick_name` varchar(150) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
`password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `name`(`name` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户管理' ROW_FORMAT = DYNAMIC;
INSERT INTO `tb_sys_user` VALUES (4, 'zhangsan', '张三', '$2a$10$dCr8Skk7kLa2kNCms.23aeiYI2RS2vrdoSae6Jz3.0w.YCiu9lmT2', NULL);
INSERT INTO `tb_sys_user` VALUES (5, 'jack', '杰克', '$2a$10$dCr8Skk7kLa2kNCms.23aeiYI2RS2vrdoSae6Jz3.0w.YCiu9lmT2', NULL);
1.3 编写接口、实体类
- 实体类
/**
* 用户管理
* @TableName tb_sys_user
*/
@TableName(value ="tb_sys_user")
@Data
public class TbSysUser implements Serializable {
/**
* 编号
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
@TableField(value = "name")
private String username;
/**
* 昵称
*/
private String nickName;
/**
* 密码
*/
private String password;
/**
* 创建时间
*/
private Date createTime;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
- Mapper接口
@Mapper
public interface TbSysUserMapper extends BaseMapper<TbSysUser> {
}
- service接口
public interface TbSysUserService extends IService<TbSysUser> {
/**
* 根据用户名称查询出用户信息
* @param username
* @return
*/
TbSysUser findUserByUsername(String username);
/**
* 登录接口
* @param tbSysUser
* @return
*/
Result login(TbSysUser tbSysUser);
}
@Service
public class TbSysUserServiceImpl extends ServiceImpl<TbSysUserMapper, TbSysUser>
implements TbSysUserService {
private final TbSysUserMapper tbSysUserMapper;
private final AuthenticationManager authenticationManager;
public TbSysUserServiceImpl(TbSysUserMapper tbSysUserMapper, AuthenticationManager authenticationManager) {
this.tbSysUserMapper = tbSysUserMapper;
this.authenticationManager = authenticationManager;
}
@Override
public TbSysUser findUserByUsername(String username) {
// 判断一下用户名称是否正确
LambdaQueryWrapper<TbSysUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(TbSysUser::getUsername, username);
List<TbSysUser> userList = tbSysUserMapper.selectList(lambdaQueryWrapper);
if (userList.size() != 1) {
throw new RuntimeException("用户名错误");
}
return userList.get(0);
}
@Override
public Result login(TbSysUser tbSysUser) {
String username = tbSysUser.getUsername();
String password = tbSysUser.getPassword();
if (!StringUtils.hasText(username)) {
return Result.error(-1, "用户名称不能为空");
}
if (!StringUtils.hasText(password)) {
return Result.error(-2, "用户密码不能为空");
}
// 封装请求参数
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(username, password);
// 手动调用认证方法
// 如果没有抛出异常,则表示认证成功,则返回一个完整对象,我们从中获取封装的UserDetails对象
try {
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
// 获取认证对象
User user = (User) authenticate.getPrincipal();
// 生成jwt
String id = UUID.randomUUID().toString().replace("-", "");
String token = JwtUtil.createJwt(id, user.getUsername());
return Result.success(0, "登录成功", token);
} catch (Exception e) {
e.printStackTrace();
}
return Result.error(-1, "用户名称或者密码错误");
}
}
1.4 修改UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名称去数据当中查询出用户信息,这里还是先模拟一下
// String name = "admin";
// String password = "admin";
// String password = "$2a$10$4/S6K/z/nF5eTk9KlF/PgOGtv2jlLGrzpO3oXINQAkNNlMqtVT6ru";
TbSysUserService sysUserService = ApplicationContextAwareUtil.getBean(TbSysUserService.class);
TbSysUser sysUser = sysUserService.findUserByUsername(username);
// 如果根据用户名称没有查询到到用户信息,则抛出异常,这里模拟操作
// 如果没有问题,则将用户信息封装成UserDetails对象
return new User(sysUser.getUsername(), sysUser.getPassword(), Collections.emptyList());
}
}
这里有一个工具类ApplicationContextAwareUtil, 从容器当中查找bean,解决一下这里可能会产生的循环依赖问题.
@Component
public class ApplicationContextAwareUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static ApplicationContext getApplicationContext(){
return context;
}
public static <T> T getBean(Class<T> clazz){
return context.getBean(clazz);
}
public static <T> T getBean(String beanName, Class<T> clazz){
return context.getBean(beanName, clazz);
}
}
1.5 修改TokenAuthenticationFilter
@Component
@Slf4j
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 1. 从请求头当中取出前端传递过来的token
String token = request.getHeader("token");
String uri = request.getRequestURI().toString();
if (uri.contains("/login")) {
filterChain.doFilter(request, response);
return;
}
// 2. 判断一下token是否为空
if (!StringUtils.hasText(token)) {
// 如果请求头当中没有传递token,直接放行.交给下一下过滤器去处理即可.
filterChain.doFilter(request, response);
return;
}
// 3. 解析token
try {
Claims claims = JwtUtil.parseJWT(token);
String id = claims.getId();
String subject = claims.getSubject();
log.info("id:{},subject:{}", id, subject);
// 解析出来subject和id了,这里的subject就是用户名称,由于这个token是由服务器下发的,服务能给发token,表示
// 肯定已经认证成功了.我们要做的是根据解析出来的token信息,去数据库查询,能不能找到匹配的信息.如果能,则表示
// 这个用户已经认证过了,直接放行就行了,如果找不到,那这个token可能是伪造的,就不能让访问资源 如果不匹配,也
// 不能访问资源
// 这里我们并没有使用数据库作为数据源,默认的用户名称和密码都是admin,不过密码是加密处理的密文而已.
// 根据用户名称查询用户信息. 【我们这里模拟一下这个操作】
// String name = "admin";
// String password = "$2a$10$4/S6K/z/nF5eTk9KlF/PgOGtv2jlLGrzpO3oXINQAkNNlMqtVT6ru";
TbSysUserService sysUserService = ApplicationContextAwareUtil.getBean(TbSysUserService.class);
TbSysUser sysUser = sysUserService.findUserByUsername(subject);
// 封装认证对象
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword(), Collections.emptyList());
// 存储用户认证凭证,已经校验通过,表示已经认证过了,不必再去执行spring security的过滤器链了.
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
// 放行操作
filterChain.doFilter(request, response);
} catch (Exception e) { // token抛出异常了,譬如说,过期了, 伪造啥的.不管是啥,我们都直接返回就行了
// 记录一下日志,直接返回即可
// 将错误信息写回给浏览器
ResponseWriteUtils.write2Client(response, Result.error(-1, "认证异常,请重新登录"));
}
}
}
1.6 修改UserController
@RestController
public class UserController {
private final TbSysUserService sysUserService;
public UserController(TbSysUserService sysUserService) {
this.sysUserService = sysUserService;
}
// private final ISysUserService sysUserService;
//
// public UserController(ISysUserService sysUserService) {
// this.sysUserService = sysUserService;
// }
@PostMapping("/api/pub/v1/login")
public Result login(@RequestBody TbSysUser sysUser){
return sysUserService.login(sysUser);
}
}
1.7 修改application.yml文件
server:
port: 9527
spring:
application:
name: spring-security-demo-02
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/rj-security-db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.rj.mapper: debug
1.8 测试
查看服务器端日志
spring security 抛出了异常了,此处后续再处理.
以上整个流程我们就完成了.
二、总结
2.1 重点内容
- 替换为真实的数据库,这里使用的是MySQL
- 跑通整个自定义认证的流程
- 在校验token的过滤器当中,会频繁的查询数据库,此时可以将认证信息存储到redis当中,避免频繁的查询数据库
2.2 下篇内容
- RBAC模型