这是Springboot与Shiro整合系列文章的第二节,本节的目标能够使用Shiro完成用户名密码的校验。
为了更加贴合实际的项目,会将用户信息存入到数据库表中
本次我们将按照以下步骤进行整合:
- 创建数据库表存储用户信息
- 引入mybatis并且生成Mapper来操作数据库表
- 创建自定义的Realm来获取认证信息
- 创建登录service来测试我们的代码
01
数据库表的创建
在这里我们只创建最基本的,必须的字段,如果实际业务上还有其他的需求,可以自己扩展该表的字段。
CREATE TABLE `sys_user` (
`userid` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` varchar(255) NOT NULL COMMENT '用户名',
`passowrd` varchar(255) NOT NULL COMMENT '密码',
`locked` bit(1) DEFAULT b'0' COMMENT '是否锁定',
`real_name` varchar(255) DEFAULT NULL COMMENT '真实姓名',
PRIMARY KEY (`userid`),
UNIQUE KEY `username_unique_index` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4;
02
Mybatis引入
在Pom.xml文件中引入以下依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
下面我们来根据数据库表来生成相应的Mapper, 这里强烈建议使用 MyBatisCodeHelperPro 插件,因为它是我用过的最好的Mybatis插件,功能十分强大。
1. 在IDEA建立数据库的连接
2. 使用MyBatisCodeHelperPro 插件创建Mapper
3. 检查插件是否帮我们生成成功相关代码
4. 在application.properties 文件中配置工程连接的数据库信息以及mybatis mapper文件的位置
spring.datasource.username=root
spring.datasource.password=****自己的密码*****
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
mybatis.mapper-locations=classpath:/mapperxml/*.xml
03
创建Realm来验证用户名密码
1.创建UserNameToken, 此类用来承载我们的用户名和密码来和Shiro的Subject来进行交互,
在这里有一个重要的概念一定要记住: token中的principal 和 credentials代表的是什么含义,那么在其他类中principal 和credentials也要代表同样的含义,否则会引起认证缓存在logout的时候无法删除的问题。
在此UserNameToken 中 principal就是用户名, credentials就是密码,所以后边的UserNamePasswordRealm中principal 也是用户名crdentials也是面命
package com.example.shirospringboot.config;
import org.apache.shiro.authc.AuthenticationToken;
//此token一定要实现AuthenticationToken
public class UserNamePasswordToken implements AuthenticationToken {
private String userName;
private String password;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public Object getPrincipal() {
return userName;
}
@Override
public Object getCredentials() {
return password;
}
}
2. 创建UserNamePasswordRealm,此Realm只支持上边创建的UserNamePasswordToken,获取用户信息以及除密码校验的信息校验逻辑都在Realm中实现
我们先看一下Realm中的大体逻辑
Realm逻辑
- 通过用户名从数据库中查询出来用户的信息
- 如果用户的信息为空或者用户被锁定或者其他用户的异常,直接抛出认证异常
- 将查询出来的用户信息,封装成为指定的类型(此处不需要比较数据库中的密码和传入的密码是否相同,Shiro的CredentialsMatcher会帮我们自动比对),
- 删除掉上次在ShiroConfig.java中占位的Realm,它会干扰到我们的测试
//删除掉它,否则会影响后边的结果
//因为默认的多Realm执行策略的问题,这个在后续文章中还会涉及到
@Bean
public Realm realm() {
TextConfigurationRealm realm = new TextConfigurationRealm();
realm.setUserDefinitions("joe.coder=password,user\n"
"jill.coder=password,admin");
realm.setRoleDefinitions("admin=read,write\n"
"user=read");
realm.setCachingEnabled(true);
return realm;
}
获取用户信息我们需要新增以下Mapper代码
SysUserMapper.java增加如下代码
SysUserselectAllByUsername(@Param("username")Stringusername)
SysUserMapper.xml 增加如下代码
<select id="selectAllByUsername" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from sys_user
where username=#{username}
</select>
- 因为此Realm只需要负责认证的工作,所以继承了AuthticatingRealm父类,如果既要负责认证又要负责验权的工作就要继承 AuthorizingRealm, 后续的Token交互方式我们会用到该类
- 因为此Realm只需要支持UserNameToken, 所以在类中覆盖了父类的supports方法
UserNamePasswordRealm.java 代码如下
package com.example.shirospringboot.config;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;
import org.springframework.stereotype.Component;
import com.example.shirospringboot.mapper.SysUserMapper;
import com.example.shirospringboot.model.SysUser;
/**
* 此realm只是用做认证使用,所以继承了AuthenticatingRealm
*/
@Component
public class UserNamePaswordRealm extends AuthenticatingRealm {
@Autowired
private SysUserMapper sysUserMapper;
//表明此Realm只是支持我们创建的UserNamePasswordRealm
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UserNamePasswordToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName = (String)token.getPrincipal();
SysUser sysUser = sysUserMapper.getAllByUsername(userName);
if (sysUser == null) {
throw new UnknownAccountException("用户名或密码错误");
}
if(sysUser.getLocked() != null && sysUser.getLocked()){
throw new LockedAccountException("该用户已经被锁定");
}
SimpleAuthenticationInfosimpleAuthenticationInfo=newSimpleAuthenticationInfo(userName,sysUser.getPassowrd(),sysUser.getRealName());
return simpleAuthenticationInfo ;
}
}
3. 手工在数据库中添加一条记录
INSERTINTO`sys_user`VALUES('23','admin','123','\0','管理员');
请记住用户名是admin 密码是123,后边的测试我们会用到
04
创建测试service
1.创建UserNamePasswordBean类用于接收前端传来的用户名和密码
package com.example.shirospringboot.requestBean;
import javax.validation.constraints.NotBlank;
public class UserNamePasswordBean {
@NotBlank(message = "密码不为空")
private String password;
@NotBlank(message="用户名不为空")
private String username;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
上述代码中的校验注解生效需要引入下面的依赖
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>7.0.1.Final</version>
</dependency>
2.创建LoginController 用来进行登录的测试
package com.example.shirospringboot.controller;
import org.apache.shiro.SecurityUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import com.example.shirospringboot.config.UserNamePasswordToken;
import com.example.shirospringboot.requestBean.UserNamePasswordBean;
@Controller
public class LoginController {
public ResponseEntity login(@RequestBody UserNamePasswordBean userNamePasswordBean) {
UserNamePasswordToken userNamePasswordToken = new UserNamePasswordToken();
userNamePasswordToken.setUserName(userNamePasswordBean.getUsername());
userNamePasswordToken.setPassword(userNamePasswordBean.getPassword());
SecurityUtils.getSubject().login(userNamePasswordToken);
return ResponseEntity.ok().build();
}
}
同时创建全局异常处理器,当认证异常的时候可以进行异常的捕获
package com.example.shirospringboot.controller;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class AppExceptionHandler {
/**
* 用来处理密码不正确
* @return
*/
@ExceptionHandler(IncorrectCredentialsException.class)
public ResponseEntity incorrectCredentialsException() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户名或密码不正确");
}
/**
* 用来处理其他的认证异常
* @param e
* @return
*/
@ExceptionHandler({AuthenticationException.class})
public ResponseEntity authenException(Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
}
}
05
测试
我们使用PostMan 进行service的测试
- 密码正确的测试
2 .密码不正确的测试
- 用户不存在的测试
测试结果基本达到了我们的预期,但是你有没有觉得好像缺点什么呢,
是的没错,为什么我们的密码是明文存储的,这很不安全。
下一篇文章,让我们一起来聊一聊Shiro是如何帮我们完成加密和解密工作的。
代码经被上传至Gitee仓库,有需要者可私信笔者
,