SpringBoot-Shiro用户认证 (2019.12.12)
在Spring Boot中集成Shiro进行用户的认证过程主要可以归纳为以下三点:
1、定义一个ShiroConfig,然后配置SecurityManager Bean,SecurityManager为Shiro的安全管理器,管理着所有Subject;
2、在ShiroConfig中配置ShiroFilterFactoryBean,其为Shiro过滤器工厂类,依赖于SecurityManager;
3、自定义Realm实现,Realm包含doGetAuthorizationInfo()
和doGetAuthenticationInfo()
方法,因为本文只涉及用户认证,所以只实现doGetAuthenticationInfo()
方法。
1. 搭建一个SpringBoot-Shiro程序,然后引入Shiro、MyBatis、数据库和thymeleaf 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!--使用阿里巴巴的德鲁伊作为数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mysql数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--Boot整合的Shiro 依赖 或者引入单独的 shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.4.0-RC2</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2. 定义一个Shiro配置类,名称为ShiroConfig:
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 登录的url
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后跳转的url
shiroFilterFactoryBean.setSuccessUrl("/index");
// 未授权url
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 定义filterChain,静态资源不拦截
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
// druid数据源监控页面不拦截(可选)
filterChainDefinitionMap.put("/druid/**", "anon");
// 配置退出过滤器,其中具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/", "anon");
// 除上以外所有url都必须认证通过才可以访问,未通过认证自动访问LoginUrl
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean //这里有个坑使用的依赖是shiro-spring-boot-xx 返回值得是DefaultWebSecurityManager
public DefaultWebSecurityManager securityManager(){
// 配置SecurityManager,并注入shiroRealm
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
return securityManager;
}
@Bean
public ShiroRealm shiroRealm(){
// 配置Realm,需自己实现
ShiroRealm shiroRealm = new ShiroRealm();
return shiroRealm;
}
}
需要注意的是filterChain基于短路机制,即最先匹配原则,如:
/user/**=anon
/user/aa=authc 永远不会执行
配置完ShiroConfig后,接下来对Realm进行实现,然后注入到SecurityManager中。
3. 配置ShiroRealm.java
自定义Realm实现只需继承AuthorizingRealm类,然后实现doGetAuthorizationInfo()和doGetAuthenticationInfo()方法即可。这两个方法名乍看有点像,authorization发音[ˌɔ:θəraɪˈzeɪʃn],为授权,批准的意思,即获取用户的角色和权限等信息;authentication发音[ɔ:ˌθentɪ’keɪʃn],认证,身份验证的意思,即登录时验证用户的合法性,比如验证用户名和密码。
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
/**
* 获取用户角色和权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
return null;
}
/**
* 登录认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取用户输入的用户名和密码
String userName = (String) token.getPrincipal();
String password = new String((char[]) token.getCredentials());
//System.out.println("用户" + userName + "认证-----ShiroRealm.doGetAuthenticationInfo");
// 通过用户名到数据库查询用户信息
User user = userMapper.findByUserName(userName);
if (user == null) {
throw new UnknownAccountException("用户名或密码错误!");
}
if (!password.equals(user.getPassword())) {
throw new IncorrectCredentialsException("用户名或密码错误!");
}
if (user.getStatus().equals("0")) {
throw new LockedAccountException("账号已被锁定,请联系管理员!");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
return info;
}
}
因为本节只讲述用户认证,所以doGetAuthorizationInfo()
方法先不进行实现。
其中UnknownAccountException
等异常为Shiro自带异常,Shiro具有丰富的运行时AuthenticationException
层次结构,可以准确指出尝试失败的原因。你可以包装在一个try/catch
块,并捕捉任何你希望的异常,并作出相应的反应。例如:
try {
currentUser.login(token);
} catch ( UnknownAccountException uae ) { ...
} catch ( IncorrectCredentialsException ice ) { ...
} catch ( LockedAccountException lae ) { ...
} catch ( ExcessiveAttemptsException eae ) { ...
} ... catch your own ...
} catch ( AuthenticationException ae ) {
//unexpected error?
}
虽然我们可以准确的获取异常信息,并根据这些信息给用户提示具体错误,但最安全的做法是在登录失败时仅向用户显示通用错误提示信息,例如“用户名或密码错误”。这样可以防止数据库被恶意扫描。
在Realm中UserMapper为Dao层,标准的做法应该还有Service层。接下来编写和数据库打交道的Dao层。
4.配置文件 application.yml
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?serverTimezone=UTC
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
#数据源其他配置
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
mybatis:
mapper-locations: mapper/*.xml
type-aliases-package: com.zhihao.entity
5. 定义实体类(User省略),接口UserMapper
外加上UserMapper.xml
配置实现
@Mapper
public interface UserMapper {
User findUserByName(String name);
}
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhihao.dao.UserMapper">
<resultMap type="com.zhihao.entity.User" id="baseUser">
<id column="id" property="id" javaType="java.lang.String" jdbcType="VARCHAR"/>
<id column="username" property="username" javaType="java.lang.String" jdbcType="VARCHAR"/>
<id column="password" property="password" javaType="java.lang.String" jdbcType="VARCHAR"/>
<id column="create_time" property="createTime" javaType="java.util.Date" jdbcType="DATE"/>
<id column="status" property="status" javaType="java.lang.String" jdbcType="VARCHAR"/>
</resultMap>
<sql id="baseAll">
id,username,password,status,create_time
</sql>
<select id="findUserByName" resultMap="baseUser" parameterType="String">
select
<include refid="baseAll"/>
from user where username = #{name}
</select>
</mapper>
6. UserController.java
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
/**
* @Author: zhihao
* @Date: 2019/12/12 13:58
* @Description: 用户登录
* @Versions 1.0
**/
@RestController
public class UserController {
private Map<String,Object> resultMap = new ConcurrentHashMap();
/**
* 登录认证
*
* @param username 用户名
* @param password 密码
* @return java.util.Map 简陋的结果包装
* @author: zhihao
* @date: 2019/12/12
* {@link #}
*/
@PostMapping("/login")
public Map login(@NotNull String name, @NotNull String password){
// 密码md5加密 (省略)
UsernamePasswordToken token = new UsernamePasswordToken(name, password);
//获取Subject对象
Subject subject = SecurityUtils.getSubject();
try {
//没有抛出异常,说明登录成功
subject.login(token);
resultMap.put("code", "success");
} catch (UnknownAccountException e){
//返回自定义认证失败的异常信息返回
resultMap.put("msg", e.getMessage());
}catch (IncorrectCredentialsException e) {
resultMap.put("msg", e.getMessage());
}catch (LockedAccountException e) {
resultMap.put("msg", e.getMessage());
}catch (AuthenticationException e) {
resultMap.put("msg", "认证失败");
}
return resultMap;
}
/**
* 登录的url 解析视图到登录页面
* @return org.springframework.web.servlet.ModelAndView
* @author: zhihao
* @date: 2019/12/12
* {@link #}
*/
@GetMapping("/login")
public ModelAndView login() {
ModelAndView view = new ModelAndView();
view.setViewName("login");
return view;
}
/**
* 退出(注销)访问根目录解析视图到登录页面
* @return org.springframework.web.servlet.ModelAndView
* @author: zhihao
* @date: 2019/12/12
* {@link #}
*/
@GetMapping("/")
public ModelAndView logins() {
ModelAndView view = new ModelAndView();
view.setViewName("login");
return view;
}
}
7. 接下来编写login.html和index.html页面
编写登录页面login.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<script type="text/javascript" src="/js/jquery-3.3.1.min.js"></script>
<body>
<div>
<div>
<input type="text" placeholder="用户名" name="username" id="username" required="required"/>
<input type="password" placeholder="密码" name="password" id="password" required="required"/>
<button onclick="login()">登录</button>
</div>
</div>
</body>
<script type="text/javascript">
function login() {
var username = $("#username").val();
var password = $("#password").val();
$.ajax({
type: "post",
url: "/login",
data: {"username": username, "password": password},
dataType: "json",
success: function (result) {
if (result.code == "success") {
location.href ='/index';
} else {
alert(result.msg);
}
}
});
}
</script>
</html>
首页:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<p>你好![[${user.userName}]]</p>
<a th:href="@{/logout}">注销</a>
</body>
</html>
8. 测试
http://localhost:8080/
http://localhost:8080/index
http://localhost:8080/aaaaaaa
http://localhost:8080/web
可发现页面都被重定向到http://localhost:8080/login:
登录成功后,点击注销连接,根据ShiroConfig的配置filterChainDefinitionMap.put("/logout", "logout")
,Shiro会自动帮我们注销用户信息,并重定向到/
路径。
扩展资料:
拦截规则anon
、authc
等为Shiro为我们实现的过滤器,具体如下表所示:
Filter Name | Class | Description |
---|---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter | 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例/static/**=anon |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter | 基于表单的拦截器;如/**=authc ,如果没有登录会跳到相应的登录页面登录 |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter | Basic HTTP身份验证拦截器 |
logout | org.apache.shiro.web.filter.authc.LogoutFilter | 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/),示例/logout=logout |
noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter | 不创建会话拦截器,调用subject.getSession(false) 不会有什么问题,但是如果subject.getSession(true) 将抛出DisabledSessionException 异常 |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter | 权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例/user/**=perms["user:create"] |
port | org.apache.shiro.web.filter.authz.PortFilter | 端口拦截器,主要属性port(80) :可以通过的端口;示例/test= port[80] ,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样 |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter | rest风格拦截器,自动根据请求方法构建权限字符串;示例/users=rest[user] ,会自动拼出user:read,user:create,user:update,user:delete权限字符串进行权限匹配(所有都得匹配,isPermittedAll) |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter | 角色授权拦截器,验证用户是否拥有所有角色;示例/admin/**=roles[admin] |
ssl | org.apache.shiro.web.filter.authz.SslFilter | SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口443;其他和port拦截器一样; |
user | org.apache.shiro.web.filter.authc.UserFilter | 用户拦截器,用户已经身份验证/记住我登录的都可;示例/**=user |
用户表:
/*
Navicat Premium Data Transfer
Source Server : 本地数据库
Source Server Type : MySQL
Source Server Version : 50540
Source Host : localhost:3306
Source Schema : shiro
Target Server Type : MySQL
Target Server Version : 50540
File Encoding : 65001
Date: 12/12/2019 17:20:50
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用户id',
`username` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用户名',
`password` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用户密码',
`status` char(1) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT '1' COMMENT '用户状态',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', '123', '321', '1', '2019-12-12 15:53:28');
SET FOREIGN_KEY_CHECKS = 1;