Shiro实战教程笔记
1. 权限管理
1.1 什么是权限管理
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或安全策略
控制用户可以访问并且只能访问被授权的资源。
1.2 什么是身份认证
身份认证
就是判断一个用户是否为合法用户的过程,最简单方式就是根据用户输入的用户名和密码,和系统存储层一致不,来判断用户身份是否正确。
1.3 什么是身份授权
身份授权
即访问控制,控制谁能访问那些资源,不同的用户应该拥有不同的资源访问权限,常见的有学校的教务管理系统:有教师,学生,管理员登录几个模块,以不同的身份登录就会显示不同的界面!
2. 什么是shiro
shiro是Apache旗下的一个开源框架,它将软件系统的安全认证相关的功能抽取出来,是一个功能强大且简单易用的Java安全框架,它可以用来进行身份验证,授权,加密和会话管理。
3. shiro核心架构
3.1 Subject
Subject即主体
,外部应用与Subject进行交互,Subject对象记录了当前操作的用户,将用户的概念理解成当前操作的主体,可能是一个浏览器的请求的用户,也可能是一个允许的程序。Subject在shiro中是一个接口,接口中定义了很多认证授权的方法,外部程序通过Subject对象进行认证和授权,而Subject对象是通过SecurityManager进行认证和授权!
3.2 SecurityManager
SecurityManager即安全管理器
,对全部的Subject对象进行安全管理,它是shiro的核心,通过SecurityManager可以完成对Subject对象的认证授权等系列操作,实质上是使用Authenticator进行认证,使用Authorizer进行授权,使用SessionManager进行会话管理!
3.3 Authenticator
Authenticator即认证器
,对用户身份进行认证,Authenticator是一个接口,shiro提供了ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足认证的大部分需求,也可以使用自定义的认证器。
3.4 Authorizer
Authorizer即授权器
,用户通过认证器认证通过,在访问功能时需要通过授权器
判断用户是否由此功能的操作权限。
3.5 Realm
Realm即领域
,相当于datasource,SecurityManager进行安全认证是需要从Realm获取用户权限数据,比如:用户的身份信息如果放到数据库中,那么就需要从数据库中获取到用户身份信息。
注:不要把Realm认为只是在里面取数据,在Realm还有认证和授权相关的代码!
3.6 SessionManager
SessionManager即会话管理
,shiro框架定义了一系列会话管理,它不依赖于web的session,所以shiro可以使用在非web的环境下,也可以将分布式的应用集中在一起管理,此特性可以实现单点登录!
3.7 SessionDao
SessionDao即会话dao
,是对session会话操作的一套接口,比如将session存储到数据库,也可以使用jdbc将session存入数据库。
3.8 CacheManager
CacheManager即缓存管理器
,将用户权限数据存储到缓存中,可以提高性能。
3.9 Cryptography
Cryptography即密码管理
,shiro提供了一套加密/解密的组件,方便开发,比如提供了常用分散列,盐值计算,加/解密等功能!
4. shiro的认证
4.1 认证
认证就是判断一个用户是否为合法用户的过程,最简单方式就是根据用户输入的用户名和密码,和系统存储层一致不,来判断用户身份是否正确。
4.2 认证的关键对象
- Subject:主体
访问系统的用户,主体可以是程序,用户等,进行认证的都称为主体。
- Principal:身份信息
是主体进行身份认证的标识,标识必须具有唯一性,如电话号码,手机号,邮箱地址,一个主体可以有多个身份,但必须有一个主身份!
- credential:凭证信息
只有主体自己知道的安全信息,如密码,证书等。
4.3 认证流程
4.4 认证的开发
创建项目并引入依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
在resources配置shiro.ini核心配置文件
[users]
admin=123456
lizhipeng=111111
huangyuane=222222
测试认证
/**
* 测试认证
* @author 隐约雷鸣
* @date 2021/1/21 11:11
*/
public class TestAuthenticator {
public static void main(String[] args) {
// 1.创建安全管理器对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 2.给安全管理器设置realm(本地IniRealm文件)
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
// 3.安全工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 4.获取主体对象
Subject subject = SecurityUtils.getSubject();
// 5.创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
// 6.用户认证
try {
subject.login(token);
System.out.println("认证状态:" + subject.isAuthenticated());
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("认证失败:用户名不存在!");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("认证失败:密码错误!");
}
}
}
4.5 自定义Realm的开发
使用本地shiro.ini开发,使用的realm就是我们本地提供的,而实际开发中,realm的数据往往是我们的数据库中读取!
1.shiro提供的Realm
2.Realm的实现类中认证是使用SimpleAccountRealm
SimpleAccountRealm 部分源码中有两个方法,分别是
认证:doGetAuthenticationInfo(AuthenticationToken token)
授权:doGetAuthorizationInfo(PrincipalCollection principals)
// 认证
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken)token;
SimpleAccount account = this.getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
// 授权
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = this.getUsername(principals);
this.USERS_LOCK.readLock().lock();
AuthorizationInfo var3;
try {
var3 = (AuthorizationInfo)this.users.get(username);
} finally {
this.USERS_LOCK.readLock().unlock();
}
return var3;
}
3.自定义Realm
public class CustomerRealm extends AuthorizingRealm {
// 认证
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
// 授权
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 在token中获取身份信息
String principal = (String) token.getPrincipal();
// 判断获取的身份信息是否与数据库一致,这里暂时使用假数据代替
if ("admin".equals(principal)) {
/**
* 如果用户名通过,返回数据库中用户信息
* principal:用户名
* credentials:密码
* realmName:当前Realm名字
*/
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, "123456", this.getName());
return authenticationInfo;
}
return null;
}
}
4.使用自定义Realm进行认证
public class TestCustomerRealmAuthenticator {
public static void main(String[] args) {
// 创建安全管理器对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 给安全管理设置Realm
securityManager.setRealm(new CustomerRealm());
// 安全工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 获取主体
Subject subject = SecurityUtils.getSubject();
// 设置token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
subject.login(token);
System.out.println(subject.isAuthenticated());
} catch (AuthenticationException e) {
e.printStackTrace();
}
}
}
4.6 对密码进行加盐和Hash
1.测试加密,加盐,加散列
public class TestShiroMD5 {
public static void main(String[] args) {
// 不加盐和散列次数
Md5Hash md5Hash1 = new Md5Hash("123456");
System.out.println(md5Hash1.toHex());
// 加盐,不加散列次数
Md5Hash md5Hash2 = new Md5Hash("123456", "X*02");
System.out.println(md5Hash2.toHex());
// 加盐,加散列次数
Md5Hash md5Hash3 = new Md5Hash("123456", "X*02", 1024);
System.out.println(md5Hash3.toHex());
}
}
自定义Realm,加入MD5+盐+hash
public class CustomerMd5Realm extends AuthorizingRealm {
// 认证
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
// 授权
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取身份信息
String principal = (String) token.getPrincipal();
if ("admin".equals(principal)) {
// md5
// return new SimpleAuthenticationInfo(principal, "e10adc3949ba59abbe56e057f20f883e", this.getName());
// md5 + 盐
// return new SimpleAuthenticationInfo(principal, "6c772d52ae59f2cedacf82d0eb60a9ff", ByteSource.Util.bytes("X*02"), this.getName());
// md5 + 盐 + hash
return new SimpleAuthenticationInfo(principal, "791113c0ca9bf9cea458f1221d0fb728", ByteSource.Util.bytes("X*02"), this.getName());
}
return null;
}
}
使用自定义Realm进行认证
public class TestCustomerMd5RealmAuthenticator {
public static void main(String[] args) {
// 创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 设置Realm使用hash凭证匹配器
CustomerMd5Realm realm = new CustomerMd5Realm();
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
// 适配器设置散列次数
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
// 注入Realm
securityManager.setRealm(realm);
// 安全管理工具设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 获取主体
Subject subject = SecurityUtils.getSubject();
// 设置token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
subject.login(token);
System.out.println("登录成功:" + subject.isAuthenticated());
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
}
}
5. shiro的授权
5.1 授权
授权,即访问控制,控制谁能访问那些资源。
5.2 授权的关键对象
-
Subject:主体
主体需要访问系统的资源。
-
Resource:资源
如系统菜单、页面、按钮、类方法、系统商品信息等。
-
Permission: 权限
规定了主体对资源的操作许可,权限离开了资源没有意义。
5.3 授权的流程
5.3 授权方式
-
基于角色的访问控制
以角色为中心进行访问控制
-
基于资源的访问控制
以资源为中心的访问控制
5.5 权限字符串
权限字符串的规则是:资源标识符:操作:资源实例标识符
。意思是对哪个资源的那个实例具有什么权限,”:“是分隔符,权限字符串也可以使用“*”通配符。
例子:
用户创建权限: user:create 或 user:create:*
用户修改实例001的权限: user:update:001
用户实例001的所有权限: user:*:001
5.6 shiro中授权的编程实现方式
- 编程式
Subject subject = SecurityUtils.getSubject();
if (subject.hasRole("admin")) {
// 有权限
} else {
// 无权限
}
- 注解式
@RequiresRoles("admin")
public void hello() {
// 有权限
}
- 标签式
<shiro:hasRole name="admin">
<!-- 有权限 -->
</shiro:hasRole>
注意:Thymeleaf中使用shiro需要额外集成
5.7 开发授权
自定义Realm, 给用户设置权限
public class CustomerMd5Realm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取主身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
System.out.println("身份信息:" + primaryPrincipal);
// 根据身份信息获取当前用户的角色信息以及权限信息
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 将数据库查到的用户信息赋值给权限对象
authorizationInfo.addRole("user");
authorizationInfo.addRole("super");
// 将数据库查到的用户信息赋值给权限对象
authorizationInfo.addStringPermission("user:*:01");
authorizationInfo.addStringPermission("product:update");
return authorizationInfo;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取身份信息
String principal = (String) token.getPrincipal();
if ("admin".equals(principal)) {
// md5
// return new SimpleAuthenticationInfo(principal, "e10adc3949ba59abbe56e057f20f883e", this.getName());
// md5 + 盐
// return new SimpleAuthenticationInfo(principal, "6c772d52ae59f2cedacf82d0eb60a9ff", ByteSource.Util.bytes("X*02"), this.getName());
// md5 + 盐 + hash
return new SimpleAuthenticationInfo(principal, "791113c0ca9bf9cea458f1221d0fb728", ByteSource.Util.bytes("X*02"), this.getName());
}
return null;
}
}
使用自定义Realm进行认证授权
public class TestCustomerMd5RealmAuthenticator {
public static void main(String[] args) {
// 创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 设置Realm使用hash凭证匹配器
CustomerMd5Realm realm = new CustomerMd5Realm();
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
// 适配器设置散列次数
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
// 注入Realm
securityManager.setRealm(realm);
// 安全管理工具设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 获取主体
Subject subject = SecurityUtils.getSubject();
// 设置token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
subject.login(token);
System.out.println("登录成功:" + subject.isAuthenticated());
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
// 授权
if (subject.isAuthenticated()) {
// 基于单个角色的权限控制
System.out.println("权限:" + subject.hasRole("user"));
// 基于多个角色的权限控制
System.out.println("权限:" + subject.hasAllRoles(Arrays.asList("user", "super")));
// 是否具有其中一个角色
boolean[] booleans = subject.hasRoles(Arrays.asList("user", "admin", "super"));
for (boolean aBoolean : booleans) {
System.out.println(aBoolean);
}
System.out.println("================");
// 基于权限字符串的访问控制
System.out.println("权限:" + subject.isPermitted("user:create:01"));
System.out.println("权限:" + subject.isPermitted("product:update:02"));
// 分别具有哪些权限
boolean[] permitted = subject.isPermitted("user:create:01", "order:create:01");
for (boolean b : permitted) {
System.out.println(b);
}
// 同时具有哪些权限
boolean permittedAll = subject.isPermittedAll("user:*:01", "product:update");
System.out.println(permittedAll);
}
}
}
6. SpringBoot整合shiro
整合思路
6.1 项目准备
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--jsp解析依赖-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
配置application.yml
server:
port: 8888
servlet:
context-path: /shiro
spring:
application:
name: shiro
mvc:
view:
prefix: /
suffix: .jsp
在webapp下配置jsp页面
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>index.jsp</title>
</head>
<body>
<h1>系统管理</h1>
<ul>
<li>用户管理</li>
<li>商品管理</li>
<li>订单管理</li>
<li>物流管理</li>
</ul>
</body>
</html>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>login.jsp</title>
</head>
<body>
<h1>登录页面</h1>
</body>
</html>
启动类
@SpringBootApplication
public class SpringbootJspShiroApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootJspShiroApplication.class, args);
}
}
6.2 整合shiro
导入依赖
<!--springboot整合shiro依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
创建shiro配置类
@Configuration
public class ShiroConfig {
/**
* 1.创建ShiroFilter
*
* @param defaultWebSecurityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 配置系统受限资源
// 配置系统公共资源
Map<String, String> map = new HashMap<>();
// authc:请求这个资源需要认证和授权 anon:请求这个资源不需要认证和授权
map.put("/user/login", "anon");
map.put("/user/register", "anon");
map.put("/**", "authc");
// 默认认证界面
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 2. 创建SecurityManager
*
* @param realm
* @return
*/
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("realm") Realm realm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 设置Realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
/**
* 3.创建自定义Realm
*
* @return
*/
@Bean(name = "realm")
public Realm getRealm() {
CustomerRealm customerRealm = new CustomerRealm();
return customerRealm;
}
}
自定义Realm
/**
* 自定义Realm
*
* @author 隐约雷鸣
* @date 2021/1/27 09:39
*/
public class CustomerRealm extends AuthorizingRealm {
/**
* 授权
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/**
* 认证
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
测试运行是否成功,如果没报错则证明整合shiro成功,访问index.jsp页面时会自动跳转login.jsp页面
6.3 shiro常见过滤器
配置缩写 | 对应的过滤器 | 功能 |
---|---|---|
身份验证相关的 | ||
anon | AnonymousFilter | 指定url可以匿名访问 |
authc | FormAuthenticationFilter | 基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储key(shiroLoginFailure) |
authcBasic | BasicHttpAuthenticationFilter | Basic HTTP身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application) |
logout | authc.LogoutFilter | 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/) |
user | UserFilter | 用户拦截器,用户已经身份验证/记住我登录的都可 |
授权相关的 | ||
roles | RolesAuthorizationFilter | 角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例“/admin/**=roles[admin]” |
perms | PermissionsAuthorizationFilter | 权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例“/user/**=perms[“user:create”]” |
port | PortFilter | 端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样 |
rest | HttpMethodPermissionFilter | rest风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll) |
ssl | SslFilter | SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样 |
noSessionCreation | NoSessionCreationAuthorizationFilter | 需要指定权限才能访问 |
6.4 登录功能实现
login.jsp
<body>
<h1>登录页面</h1>
<form method="post" action="${pageContext.request.contextPath}/user/login" >
用户名:<input name="username" type="text"> <br/>
密码: <input name="password" type="password"> <br/>
<input type="submit" value="登录">
</form>
</body>
controller
@Controller
@RequestMapping("user")
public class UserController {
/**
* 登录
*
* @param username
* @param password
* @return
*/
@PostMapping("/login")
public String login(String username, String password) {
// 获取主体
Subject subject = SecurityUtils.getSubject();
// 登录
try {
subject.login(new UsernamePasswordToken(username, password));
return "redirect:/index.jsp";
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误!");
}
return "redirect:/login.jsp";
}
}
自定义Realm简单实现认证
/**
* 认证
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
if ("admin".equals(principal)) {
return new SimpleAuthenticationInfo(principal, "123456", this.getName());
}
return null;
}
6.5 注销功能实现
Index.jsp
<a href="${pageContext.request.contextPath}/user/logout">注销</a>
controller
/**
* 注销
*
* @return
*/
@GetMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/login.jsp";
}
6.6 基于数据库实现用户注册功能
创建用户表
CREATE TABLE `t_user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(40) DEFAULT NULL,
`password` varchar(40) DEFAULT NULL,
`salt` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
添加依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.2</version>
</dependency>
yml配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///shiro?useUniCode=true&useSSL=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: 1234567890
mybatis:
type-aliases-package: com.lzp.springboot_jsp_shiro.entity
mapper-locations: classpath:com/lzp/mapper/*.xml
准备注册页面
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>register.jsp</title>
</head>
<body>
<h1>注册页面</h1>
<form method="post" action="${pageContext.request.contextPath}/user/register">
用户名:<input name="username" type="text"> <br/>
密码: <input name="password" type="password"> <br/>
<input type="submit" value="立即注册">
</form>
</body>
</html>
用户实体类
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
private String salt;
}
生成随机盐工具类
public class SaltUtils {
/**
* 生成随机盐
*
* @param n
* @return
*/
public static String getSalt(int n) {
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!@#$%^&*()".toCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
char aChar = chars[new Random().nextInt(chars.length)];
sb.append(aChar);
}
return sb.toString();
}
public static void main(String[] args) {
System.out.println(getSalt(4));
}
}
controller
/**
* 注册
*
* @param user
* @return
*/
@PostMapping("/register")
public String register(User user) {
try {
userService.save(user);
return "redirect:/login.jsp";
} catch (Exception e) {
e.printStackTrace();
return "redirect:/register.jsp";
}
}
service
public interface UserService {
/**
* 保存用户
*
* @param user
*/
void save(User user);
}
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
/**
* 保存用户
* @param user
*/
@Override
public void save(User user) {
// 生成随机盐
String salt = SaltUtils.getSalt(8);
user.setSalt(salt);
// 明文密码进行md5 + 盐 + hash
Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);
user.setPassword(md5Hash.toHex());
userMapper.save(user);
}
}
mapper
@Mapper
@Repository
public interface UserMapper {
/**
* 保存用户
*/
void save(User user);
}
<mapper namespace="com.lzp.springboot_jsp_shiro.mapper.UserMapper">
<insert id="save" parameterType="User" useGeneratedKeys="true" keyProperty="id">
insert into t_user values (#{id}, #{username}, #{password}, #{salt});
</insert>
</mapper>
6.7 基于数据库重新实现用户认证功能
自定义Realm修改认证逻辑
/**
* 认证
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
User user = userService.findByUserName(principal);
if (user != null) {
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
}
return null;
}
service
/**
* 根据用户名查询
*
* @param principal
*/
User findByUserName(String principal);
/**
* 根据用户名查询
*
* @param username
*/
@Override
public User findByUserName(String username) {
return userMapper.findByUserName(username);
}
mapper
/**
* 根据用户名查询
*
* @param username
*/
User findByUserName(String username);
<select id="findByUserName" parameterType="String" resultType="User">
select * from t_user where username = #{username};
</select>
ShiroConfig修改凭证匹配器,适用于MD5 + 盐 + Hash
@Bean(name = "realm")
public Realm getRealm() {
CustomerRealm customerRealm = new CustomerRealm();
// 修改凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法为MD5
credentialsMatcher.setHashAlgorithmName("MD5");
// 设置hash散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}
6.8 授权开发
1.前端页面的授权(基于角色的访问控制)
自定义Realm设置用户对应角色权限
/**
* 授权
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
// 获取角色信息
if ("admin".equals(primaryPrincipal)) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 给admin用户添加admin角色权限
authorizationInfo.addRole("admin");
return authorizationInfo;
}
return null;
}
jsp页面添加标签控制访问权限
<body>
<h1>系统管理</h1>
<a href="${pageContext.request.contextPath}/user/logout">注销</a>
<ul>
<!-- 用户管理只允许user,admin用户看到 -->
<shiro:hasAnyRoles name="user,admin">
<li><a href="">用户管理</li>
</shiro:hasAnyRoles>
<shiro:hasRole name="admin">
<li><a href="">商品管理</li>
<li><a href="">订单管理</li>
<li><a href="">物流管理</li>
</shiro:hasRole>
</ul>
</body>
2.前端页面的授权(基于权限字符串的访问控制)
自定义Realm设置用户角色的权限字符串
/**
* 授权
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
// 获取角色信息
if ("admin".equals(primaryPrincipal)) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 基于角色的权限管理:给admin用户添加admin角色权限
authorizationInfo.addRole("user");
// 基于权限字符串的权限控制:给用户角色添加用户管理的所有权限
authorizationInfo.addStringPermission("user:*:*");
return authorizationInfo;
}
return null;
}
jsp页面添加标签控制访问权限
<body>
<h1>系统管理</h1>
<a href="${pageContext.request.contextPath}/user/logout">注销</a>
<ul>
<shiro:hasAnyRoles name="user,admin">
<li><a href="">用户管理</a>
<ul>
<shiro:hasPermission name="user:create:*">
<li><a href="">添加用户</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:update:*">
<li><a href="">修改用户</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:delete:*">
<li><a href="">删除用户</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:select:*">
<li><a href="">查询用户</a></li>
</shiro:hasPermission>
</ul>
</li>
</shiro:hasAnyRoles>
<shiro:hasRole name="admin">
<li><a href="">商品管理</a></li>
<li><a href="">订单管理</a></li>
<li><a href="">物流管理</a></li>
</shiro:hasRole>
</ul>
</body>
3.后端的授权(基于角色的访问控制)
controller
@Controller
@RequestMapping("order")
public class OrderController {
@GetMapping("/create")
public String createOrder() {
Subject subject = SecurityUtils.getSubject();
if (subject.hasRole("admin")) {
System.out.println("保存订单");
} else {
System.out.println("无权访问");
}
return "redirect:/index.jsp";
}
}
自定义Realm设置用户角色为admin
/**
* 授权
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
// 获取角色信息
if ("admin".equals(primaryPrincipal)) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 基于角色的权限管理:给admin用户添加admin角色权限
authorizationInfo.addRole("admin");
// 基于权限字符串的权限控制:给用户角色添加用户管理的所有权限
authorizationInfo.addStringPermission("user:*:*");
return authorizationInfo;
}
return null;
}
注解方式
@GetMapping("/update")
// @RequiresRoles("admin") // 要求admin角色才能访问
@RequiresRoles(value = {"admin", "user"}) // 同时有admin和user角色才能访问
public String updateOrder() {
System.out.println("更新成功");
return "redirect:/index.jsp";
}
4.后端的授权(基于权限字符串的访问控制)
controller
@GetMapping("/delete")
@RequiresPermissions("user:delete:*") // 要求该用户有user:delete:*权限
public String deleteOrder() {
System.out.println("删除成功");
return "redirect:/index.jsp";
}
6.9 授权数据持久化
创建数据库表
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80020
Source Host : localhost:3306
Source Schema : shiro
Target Server Type : MySQL
Target Server Version : 80020
File Encoding : 65001
Date: 31/01/2021 22:09:41
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_perms`;
CREATE TABLE `t_perms` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(80) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(60) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_role_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_role_perms`;
CREATE TABLE `t_role_perms` (
`id` int NOT NULL AUTO_INCREMENT,
`roleid` int DEFAULT NULL,
`permsid` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(40) DEFAULT NULL,
`password` varchar(40) DEFAULT NULL,
`salt` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`id` int NOT NULL AUTO_INCREMENT,
`userid` int DEFAULT NULL,
`roleid` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
创建对应实体类并建立对应关系
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
private String salt;
// 定义角色集合
private List<Role> roles;
}
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class Role {
private Integer id;
private String name;
// 定义权限集合
private List<Perms> permsList;
}
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class Perms {
private Integer id;
private String name;
private String url;
}
6.10 基于数据库重新实现授权流程
自定义Realm修改授权流程
/**
* 授权
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
// 获取角色信息
User user = userService.findRolesByUserName(primaryPrincipal);
if (!CollectionUtils.isEmpty(user.getRoles())) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
user.getRoles().forEach(role -> {
authorizationInfo.addRole(role.getName());
// 根据角色id设置对应的权限信息
List<Perms> permsList = userService.findPermsListByRoleId(role.getId());
if (!CollectionUtils.isEmpty(permsList)) {
permsList.forEach(perms -> {
authorizationInfo.addStringPermission(perms.getName());
});
}
});
return authorizationInfo;
}
return null;
}
service
/**
* 根据用户名获取所有角色信息
*
* @param primaryPrincipal
* @return
*/
User findRolesByUserName(String primaryPrincipal);
/**
* 根据角色id查询所有权限信息
*
* @param id
* @return
*/
List<Perms> findPermsListByRoleId(Integer id);
/**
* 根据用户名获取所有角色信息
*
* @param username
* @return
*/
@Override
public User findRolesByUserName(String username) {
return userMapper.findRolesByUserName(username);
}
/**
* 根据角色id查询所有权限信息
*
* @param id
* @return
*/
@Override
public List<Perms> findPermsListByRoleId(Integer id) {
return userMapper.findPermsListByRoleId(id);
}
mapper
/**
* 根据用户名获取所有角色信息
*
* @param username
* @return
*/
User findRolesByUserName(String username);
/**
* 根据角色id查询所有权限信息
*
* @param id
* @return
*/
List<Perms> findPermsListByRoleId(Integer id);
<resultMap id="userMap" type="User">
<id column="uid" property="id"></id>
<result column="username" property="username"></result>
<!-- 角色信息 -->
<collection property="roles" javaType="List" ofType="Role">
<id column="rid" property="id"></id>
<result column="rname" property="name"></result>
</collection>
</resultMap>
<select id="findRolesByUserName" parameterType="String" resultMap="userMap">
SELECT u.id uid, u.username, r.id rid, r.`name` rname FROM t_user u
LEFT JOIN t_user_role ur
ON u.id = ur.userid
LEFT JOIN t_role r
ON ur.roleid = r.id
WHERE u.username = #{username}
</select>
<select id="findPermsListByRoleId" parameterType="Integer" resultType="Perms">
SELECT r.`name` rname, p.id, p.name, p.url FROM t_role r
LEFT JOIN t_role_perms rp
ON r.id = rp.roleid
LEFT JOIN t_perms p
ON rp.permsid = p.id
WHERE r.id = #{id}
</select>
7. 缓存管理器
7.1 使用默认EhCache实现缓存
引入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>
ShiroConfig中配置缓存管理器
/**
* 3.创建自定义Realm
*
* @return
*/
@Bean(name = "realm")
public Realm getRealm() {
CustomerRealm customerRealm = new CustomerRealm();
// 修改凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法为MD5
credentialsMatcher.setHashAlgorithmName("MD5");
// 设置hash散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
// 开启缓存管理器
customerRealm.setCacheManager(new EhCacheManager());
customerRealm.setCachingEnabled(true);
customerRealm.setAuthenticationCachingEnabled(true);
customerRealm.setAuthenticationCacheName("authenticationCache");
customerRealm.setAuthorizationCachingEnabled(true);
customerRealm.setAuthorizationCacheName("authorizationCache");
return customerRealm;
}
7.2 使用Redis实现缓存
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
yml新增redis配置
spring:
redis:
port: 6379
host: localhost
database: 0
自定义RedisCacheManager实现CacheManage
public class RedisCacheManager implements CacheManager {
/**
* @param cacheName 认证或者是授权缓存的统一名称
* @param <K>
* @param <V>
* @return
* @throws CacheException
*/
@Override
public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
System.out.println("cacheName:" + cacheName);
return new RedisCache<K, V>(cacheName);
}
}
自定义RedisCache实现Cache
public class RedisCache<k, v> implements Cache<k, v> {
private String cacheName;
public RedisCache() {
}
public RedisCache(String cacheName) {
this.cacheName = cacheName;
}
@Override
public v get(k k) throws CacheException {
System.out.println("get:key: " + k);
return (v) getRedisTemplate().opsForHash().get(this.cacheName, k.toString());
}
@Override
public v put(k k, v v) throws CacheException {
System.out.println("put:key: " + k);
System.out.println("put:value: " + v);
getRedisTemplate().opsForHash().put(this.cacheName, k.toString(), v);
return null;
}
@Override
public v remove(k k) throws CacheException {
return (v) getRedisTemplate().opsForHash().delete(this.cacheName, k.toString());
}
@Override
public void clear() throws CacheException {
getRedisTemplate().delete(this.cacheName);
}
@Override
public int size() {
return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
}
@Override
public Set<k> keys() {
return getRedisTemplate().opsForHash().keys(this.cacheName);
}
@Override
public Collection<v> values() {
return getRedisTemplate().opsForHash().values(this.cacheName);
}
/**
* 获取RedisTemplate
*
* @return
*/
private RedisTemplate getRedisTemplate() {
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
ApplicationContextUtils工具类,因为无法自动注入redisTemplate
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
//根据bean名称从工厂中获取bean对象
public static Object getBean(String name){
return applicationContext.getBean(name);
}
}
自定义Salt的实现,实现序列化接口,提供无参构造,不然会报错反序列化异常,其它实体类也需要实现序列化接口
public class MyByteSource implements ByteSource, Serializable {
private byte[] bytes;
private String cachedHex;
private String cachedBase64;
public MyByteSource() {
}
public MyByteSource(byte[] bytes) {
this.bytes = bytes;
}
public MyByteSource(char[] chars) {
this.bytes = CodecSupport.toBytes(chars);
}
public MyByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public MyByteSource(ByteSource source) {
this.bytes = source.getBytes();
}
public MyByteSource(File file) {
this.bytes = (new MyByteSource.BytesHelper()).getBytes(file);
}
public MyByteSource(InputStream stream) {
this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream);
}
public static boolean isCompatible(Object o) {
return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
}
public byte[] getBytes() {
return this.bytes;
}
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
public String toHex() {
if (this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}
return this.cachedHex;
}
public String toBase64() {
if (this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
public String toString() {
return this.toBase64();
}
public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof ByteSource) {
ByteSource bs = (ByteSource)o;
return Arrays.equals(this.getBytes(), bs.getBytes());
} else {
return false;
}
}
private static final class BytesHelper extends CodecSupport {
private BytesHelper() {
}
public byte[] getBytes(File file) {
return this.toBytes(file);
}
public byte[] getBytes(InputStream stream) {
return this.toBytes(stream);
}
}
}
CustomerRealm认证时使用自定义的salt实现
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
User user = userService.findByUserName(principal);
if (user != null) {
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), new MyByteSource(user.getSalt()), this.getName());
}
return null;
}
修改ShiroConfig,使用RedisCacheManager实现
@Bean(name = "realm")
public Realm getRealm() {
CustomerRealm customerRealm = new CustomerRealm();
// 修改凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法为MD5
credentialsMatcher.setHashAlgorithmName("MD5");
// 设置hash散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
// 开启缓存管理器
customerRealm.setCacheManager(new RedisCacheManager());
customerRealm.setCachingEnabled(true);
customerRealm.setAuthenticationCachingEnabled(true);
customerRealm.setAuthenticationCacheName("authenticationCache");
customerRealm.setAuthorizationCachingEnabled(true);
customerRealm.setAuthorizationCacheName("authorizationCache");
return customerRealm;
}
测试登录
登录成功后,会在Redis中存入authenticationCache和com.lzp.springboot_jsp_shiro.shiro.realms.CustomerRealm.authorizationCache这两个key

7.3 验证码登录
login.jsp
<body>
<h1>登录页面</h1>
<form method="post" action="${pageContext.request.contextPath}/user/login" >
用户名:<input name="username" type="text"> <br/>
密码: <input name="password" type="password"> <br/>
验证码:<input type="text" name="cdoe"><img src="${pageContext.request.contextPath}/user/getImage" alt=""><br>
<input type="submit" value="登录">
</form>
</body>
验证码工具类
public class VerifyCodeUtils {
//使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static Random random = new Random();
/**
* 使用系统默认字符源生成验证码
*
* @param verifySize 验证码长度
* @return
*/
public static String generateVerifyCode(int verifySize) {
return generateVerifyCode(verifySize, VERIFY_CODES);
}
/**
* 使用指定源生成验证码
*
* @param verifySize 验证码长度
* @param sources 验证码字符源
* @return
*/
public static String generateVerifyCode(int verifySize, String sources) {
if (sources == null || sources.length() == 0) {
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for (int i = 0; i < verifySize; i++) {
verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
}
return verifyCode.toString();
}
/**
* 生成随机验证码文件,并返回验证码值
*
* @param w
* @param h
* @param outputFile
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException {
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, outputFile, verifyCode);
return verifyCode;
}
/**
* 输出随机验证码图片流,并返回验证码值
*
* @param w
* @param h
* @param os
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, os, verifyCode);
return verifyCode;
}
/**
* 生成指定验证码图像文件
*
* @param w
* @param h
* @param outputFile
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, File outputFile, String code) throws IOException {
if (outputFile == null) {
return;
}
File dir = outputFile.getParentFile();
if (!dir.exists()) {
dir.mkdirs();
}
try {
outputFile.createNewFile();
FileOutputStream fos = new FileOutputStream(outputFile);
outputImage(w, h, fos, code);
fos.close();
} catch (IOException e) {
throw e;
}
}
/**
* 输出指定验证码图片流
*
* @param w
* @param h
* @param os
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN,
Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
Color.PINK, Color.YELLOW};
float[] fractions = new float[colors.length];
for (int i = 0; i < colors.length; i++) {
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions);
g2.setColor(Color.GRAY);// 设置边框色
g2.fillRect(0, 0, w, h);
Color c = getRandColor(200, 250);
g2.setColor(c);// 设置背景色
g2.fillRect(0, 2, w, h - 4);
//绘制干扰线
Random random = new Random();
g2.setColor(getRandColor(160, 200));// 设置线条的颜色
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}
// 添加噪点
float yawpRate = 0.05f;// 噪声率
int area = (int) (yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
}
shear(g2, w, h, c);// 使图片扭曲
g2.setColor(getRandColor(100, 160));
int fontSize = h - 4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for (int i = 0; i < verifySize; i++) {
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
}
g2.dispose();
ImageIO.write(image, "jpg", os);
}
private static Color getRandColor(int fc, int bc) {
if (fc > 255)
fc = 255;
if (bc > 255)
bc = 255;
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
}
private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
}
private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
private static void shearX(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(2);
boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);
for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
}
private static void shearY(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(40) + 10; // 50;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
public static void main(String[] args) throws IOException {
File dir = new File("D:/upload/verifyCode");
int w = 200, h = 80;
for (int i = 0; i < 50; i++) {
String verifyCode = generateVerifyCode(4);
File file = new File(dir, verifyCode + ".jpg");
outputImage(w, h, file, verifyCode);
}
}
}
controller
/**
* 登录
*
* @param username
* @param password
* @return
*/
@PostMapping("/login")
public String login(String username, String password, String code, HttpSession session) {
try {
// 校验验证码
String codes = (String) session.getAttribute("code");
if (codes.equalsIgnoreCase(code)) {
// 获取主体
Subject subject = SecurityUtils.getSubject();
subject.login(new UsernamePasswordToken(username, password));
return "redirect:/index.jsp";
} else {
throw new RuntimeException("验证码错误!");
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误!");
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.getMessage());
}
return "redirect:/login.jsp";
}
/**
* 验证码
*
* @param session
* @param response
*/
@GetMapping("/getImage")
public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
// 生成验证码存入session
String code = VerifyCodeUtils.generateVerifyCode(4);
session.setAttribute("code", code);
ServletOutputStream outputStream = response.getOutputStream();
// 验证码存入图片
response.setContentType("image/png");
VerifyCodeUtils.outputImage(200, 30, outputStream, code);
}