声明
博文的内容学习自B站UP主 编程不良人 (UID:352224540)的教程,感谢能有这么好的教程
https://www.bilibili.com/video/BV1uz4y197Zm?p=9
Shiro
shiro是一个基于java的安全框架,主要提供认证与授权服务。
认证——概念
在shiro框架中,系统的访问者被称为 Subject (主体)
- Subject 的用户名属于 Principal (身份信息)
- Subject 的密码属于 Credential(凭证信息)
- Token (令牌) = Principal + Credential + ...
每次访问系统时,Subject 都会带着它的令牌给 Shiro 进行 Authentication (身份认证),
Shiro 会派出 SecurityManager(安全管理器) 来处理 Subject 的令牌,
SecurityManager 会到 Realm(数据域)中核实令牌的信息,如果核实通过,意味着认证成功了,则被允许进入系统
实操
这里用javaSE来使用shiro,创建maven工程
工程目录结构如下:
Shiro
--shiro-java
----src
------main
--------java
----------com
------------xilo
--------------shiro
----------------TestAuthenticator.java
--------resources
----------shiro.ini
----pom.xml(导入依赖)
--pom.xml
导入shiro依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
编写数据文件shiro.ini(这是shiro最原始的数据配置文件,数据都是提前写好的,实际肯定极少用这种文件,要接数据库)
以键值对的形式记录:用户名 = 密码
[users]
jack=001
lucy=002
编写认证类 TestAuthenticator.java
package com.xilo.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
public class TestAuthenticator {
public static void main(String[] args) {
//1、创建安全管理器对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2、给安全管理器设置realm
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
//3、给全局安全工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//4、关键对象 Subject 主体
Subject subject = SecurityUtils.getSubject();
//5、创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("jack","001");
//用户登录(进行验证)
try{
subject.login(token);
System.out.println("认证状态:"+subject.isAuthenticated());
}catch (Exception e){
e.printStackTrace();
}
}
}
到此就可以运行工程了,运行结果:
认证状态:true
如果是填错误信息,会报什么错误呢?
//5、创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("jack001","001");
运行结果:
org.apache.shiro.authc.UnknownAccountException: Realm [org.apache.shiro.realm.text.IniRealm@573fd745] was unable to find account data for the submitted AuthenticationToken [org.apache.shiro.authc.UsernamePasswordToken - jack001, rememberMe=false].
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:184)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:273)
at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)
at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)
at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:275)
at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:260)
at com.xilo.shiro.TestAuthenticator.main(TestAuthenticator.java:27)
可以看到会报出一个异常 UnknownAccountException ,这个异常就代表着用户不存在的异常
接下来我们再试试密码错误的情况
//5、创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("jack","003");
运行结果:
org.apache.shiro.authc.IncorrectCredentialsException: Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - jack, rememberMe=false] did not match the expected credentials.
at org.apache.shiro.realm.AuthenticatingRealm.assertCredentialsMatch(AuthenticatingRealm.java:603)
at org.apache.shiro.realm.AuthenticatingRealm.getAuthenticationInfo(AuthenticatingRealm.java:581)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:180)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:273)
at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)
at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)
at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:275)
at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:260)
at com.xilo.shiro.TestAuthenticator.main(TestAuthenticator.java:27)
Process finished with exit code 0
可以看到会报出一个异常 IncorrectCredentialsException,这个异常就代表着密码错误的异常。
我们可以利用这两个异常把具体信息反馈给操作者
package com.xilo.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
public class TestAuthenticator {
public static void main(String[] args) {
//1、创建安全管理器对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2、给安全管理器设置realm
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
//3、给全局安全工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//4、关键对象 Subject 主体
Subject subject = SecurityUtils.getSubject();
//5、创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("jack","001");
//用户登录(进行验证)
try{
subject.login(token);
System.out.println("登录成功");
}catch (UnknownAccountException e){
System.out.println("用户"+token.getUsername()+"不存在");
}catch (IncorrectCredentialsException e){
System.out.println("用户"+token.getUsername()+"密码错误");
}catch (Exception e){
e.printStackTrace();
}
}
}
自定义Realm
在上面的例子中,我们采用了shiro.ini文件的形式作为shiro的Realm,完成对用户的认证,但在实际的开发中是极少用到这种形式的,一般会采用数据库来记录我们的用户信息,所以我们要编写自定义Realm,让Realm去数据库查询相关的数据,完成对用户的信息认证。
下面放出改动后的目录结构
Shiro
--shiro-java
----src
------main
--------java
----------com
------------xilo
--------------realm
----------------CustomerRealm.java(自定义Realm)
--------------shiro
----------------TestAuthenticator.java
----------------TestCustomerRealmAuthenticator.java(使用自定义Realm的shiro)
--------resources
----------shiro.ini
----pom.xml
--pom.xml
实现自定义的Realm,就要继承AuthorizingRealm,重载两个重要的方法:doGetAuthenticationInfo(用于认证) 和 doGetAuthorizationInfo(用于授权,这个后面再说)
CustomerRealm.java
package com.xilo.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class CustomerRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//认证操作
return null;
}
}
TestCustomerRealmAuthenticator.java
package com.xilo.shiro;
import com.xilo.realm.CustomerRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
public class TestCustomerRealmAuthenticator {
public static void main(String[] args) {
//1、创建安全管理器对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2、给安全管理器设置【自定义realm】
securityManager.setRealm(new CustomerRealm());
//3、给全局安全工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//4、关键对象 Subject 主体
Subject subject = SecurityUtils.getSubject();
//5、创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("jack","001");
//用户登录(进行验证)
try{
subject.login(token);
System.out.println("认证状态:"+subject.isAuthenticated());
}catch (UnknownAccountException e){
System.out.println("用户"+token.getUsername()+"不存在");
}catch (IncorrectCredentialsException e){
System.out.println("用户"+token.getUsername()+"密码错误");
}catch (Exception e){
e.printStackTrace();
}
}
}
这时候,我们写的自定义Realm的认证内容是空的,返回null
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//认证操作
return null;
}
当我们仍然可以运行一下 TestCustomerRealmAuthenticator.java ,看看结果怎么样
用户jack不存在
预料之内,没有找到用户的相关信息,我们可以验证一下 doGetAuthenticationInfo 是否真的做了认证的工作。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String)token.getPrincipal();
System.out.println("doGetAuthenticationInfo---用户名:"+principal);
return null;
}
运行之后可以看到 doGetAuthenticationInfo 拿到了用户登录的token
doGetAuthenticationInfo---用户名:jack
用户jack不存在
我们可以在 doGetAuthenticationInfo 中用token去验证数据库中的数据(利用jdbc、jpa、Mybatis等)
在这里,我们先用 SimpleAuthenticationInfo 模拟数据库里的用户信息,完成密码的校验
Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String)token.getPrincipal();
System.out.println("doGetAuthenticationInfo---用户名:"+principal);
if("jack".equals(principal)){
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("jack","123",this.getName());
return simpleAuthenticationInfo;
}
return null;
}
运行之后:
doGetAuthenticationInfo---用户名:jack
用户jack密码错误
贴出CustomerRealm.java 完整代码
package com.xilo.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class CustomerRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String)token.getPrincipal();
System.out.println("doGetAuthenticationInfo---用户名:"+principal);
if("jack".equals(principal)){
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("jack","123",this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
密码加密处理
在上面我们简单模拟了用shiro认证用户的操作,但其中有个地方是与实际不太切合的,就是密码的存储是以明文的形式存储,这样会造成很大的安全隐患,所以我们要考虑给密码进行加密处理,这里推荐用MD5算法
MD5 算法:
是不可逆的加密算法,只能明文转成密文,不能密文转成明文,而且相同内容的明文会转成相同的密文。
密文是一个16进制的32位长度的字符串。
可以对任何文件的内容进行加密。
经过了MD5加密之后,得到的密文确实安全一点,但为了让密文更复杂,我们可以通过加Salt(盐) 到密码明文中,然后再进行MD5加密,当然在用户注册的同时也要记录下Salt的值
Salt 加盐:
在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。
试用MD5(下面使用MD5Hash,是具有hash散列算法的MD5,也是shiro推荐使用的)
package com;
import org.apache.shiro.crypto.hash.Md5Hash;
public class TestMD5Hash {
public static void main(String[] args) {
//MD5加密
Md5Hash md5Hash = new Md5Hash("001");
System.out.println(md5Hash.toHex());
//MD5 + salt 加密(默认散列1次)
md5Hash = new Md5Hash("001","01*1334px");
System.out.println(md5Hash.toHex());
//MD5 + salt + hash(散列1024次) 加密
md5Hash = new Md5Hash("001","01*1334px",1024);
System.out.println(md5Hash.toHex());
}
}
运行结果
dc5c7986daef50c1e02ab09b442ee34f
fd5382bdb301d6e2bc916e06f6455607
923391846b52b2391ba3a3cfca311ab9
保留上面的结果和盐值,下面还要用
自定义带MD5的Realm
下面我们就把MD5的密码验证加入到shiro中。
改动后的目录结构:
Shiro
--shiro-java
----src
------main
--------java
----------com
------------xilo
--------------realm
----------------CustomerRealm.java
----------------CustomerMD5Realm.java(MD5Realm)
--------------shiro
----------------TestAuthenticator.java
----------------TestCustomerRealmAuthenticator.java
----------------TestCustomerMD5RealmAuthenticator.java(使用自定义MD5Realm的shiro)
--------resources
----------shiro.ini
----pom.xml
--pom.xml
CustomerMD5Realm.java (注意下面的密码是经过MD5处理过的)
package com.xilo.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class CustomerMD5Realm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String)token.getPrincipal();
System.out.println("doGetAuthenticationInfo---用户名:"+principal);
if("jack".equals(principal)){
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("jack","dc5c7986daef50c1e02ab09b442ee34f",this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
TestCustomerMD5RealmAuthenticator.java
package com.xilo.shiro;
import com.xilo.realm.CustomerMD5Realm;
import com.xilo.realm.CustomerRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
public class TestCustomerMD5RealmAuthenticator {
public static void main(String[] args) {
//1、创建安全管理器对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2、创建Realm
CustomerMD5Realm realm = new CustomerMD5Realm();
//3、设置Realm使用hash凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
realm.setCredentialsMatcher(credentialsMatcher);
//4、给安全管理器设置【自定义realm】
securityManager.setRealm(realm);
//5、给全局安全工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//6、关键对象 Subject 主体
Subject subject = SecurityUtils.getSubject();
//7、创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("jack","001");
//用户登录(进行验证)
try{
subject.login(token);
System.out.println("认证状态:"+subject.isAuthenticated());
}catch (UnknownAccountException e){
System.out.println("用户"+token.getUsername()+"不存在");
}catch (IncorrectCredentialsException e){
System.out.println("用户"+token.getUsername()+"密码错误");
}catch (Exception e){
e.printStackTrace();
}
}
}
运行:
doGetAuthenticationInfo---用户名:jack
认证状态:true
认证通过,下面再试试加盐验证,要修改一下 CustomerMD5Realm.java (修改密码,添加盐)
if("jack".equals(principal)){
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("jack","fd5382bdb301d6e2bc916e06f6455607", ByteSource.Util.bytes("01*1334px"),this.getName());
return simpleAuthenticationInfo;
}
运行之后仍然成功(这里就不贴结果了),那再加上散列呢?
修改一下 CustomerMD5Realm.java (修改密码)
if("jack".equals(principal)){
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("jack","923391846b52b2391ba3a3cfca311ab9", ByteSource.Util.bytes("01*1334px"),this.getName());
return simpleAuthenticationInfo;
}
还要修改一下 TestCustomerMD5RealmAuthenticator.java (设置散列次数)
//3、设置Realm使用hash凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
//散列1024次
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
运行之后仍然成功(这里就不贴结果了)