安全框架常见的由两种,一种是shiro,另一种是spring security。
shiro相较于后者更加简洁,spring security可以无缝的整合spring框架
0 shiro介绍
Apache Shiro是Java 的一个安全框架。Shiro可以非常容易的开发出足够好的应用,其不仅可以用在]avaSE环境,也可以用在]avaEE环境。Shiro可以帮助我们完成:认证【登陆】、授权【权限】、加密【密码】、会话管理、与Web集成、缓存等。
1 shiro中常见的概念
Subject:
Subject主体,外部应用与Subject进行交互,Subject将用户作为当前操作的主体,这个主体:可以是一个通过浏览器请求的用户,也可能是一个运行的程序。Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过Subject进行认证授,而Subject是通过SecurityManager安全管理器进行认证授权。
SecurityManager:
SecurityManager权限管理器,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等。SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。SecurityManager是一个接口,继承了Authenticator,Authorizer,SessionManager这三个接口。
Authenticator:
Authenticator即认证器,对用户登录时进行身份认证。
Authorizer:
Authorizer授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
Realm(数据库读取+认证功能+授权功能实现):
Realm领域,相当于datasource数据源,SecurityManager进行安全认证需要通过Realm获取用户权限数据。
比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。
SessionManager:
SessionManager会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
SessionDAO:
SessionDAO即会话dao,是对session会话操作的一套接口。
比如:可以通过jdbc将会话存储到数据库也可以把session存储到缓存服务器。
CacheManager:
cacheManager缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
Cryptography:
Cryptography密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。
2 快速入门shiro
2.1 认证的流程
流程如下:
首先调用 Subject.login(token)进行登录,其会自动委托给 Security Manager,调用之前必须通过 Securityutils.setSecurityManager()设置;
SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
Authenticator可能会委托给相应的AuthenticationStrategy 进行多Realm身份验证,默认ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
Authenticator会把相应的 token传入 Realm,从 Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。
(1)在maven项目中引入相关依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro-version}</version>
</dependency>
(2)创建.ini文件模拟数据
[users]===表示user表
admin=123456
zs=123456
(3)编写代码测试
package test;
public class test01 {
public static void main(String[] args) {
//创建SecurityManager对象。
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//为SecurityManager指定Realm对象,这里要使用classpath指定编译后的资源文件
IniRealm realm=new IniRealm("classpath:shiro.ini");
//通过SecurityUtil把SecurityManager设置上下文可用。生效问题。
SecurityUtils.setSecurityManager(securityManager);
//获取Subject对象
Subject subject = SecurityUtils.getSubject();
//完成认证功能AuthenticationToken:该类把账号和密码封装到该类中
UsernamePasswordToken token=new UsernamePasswordToken("admin","123456");
try {
subject.login(token);
System.out.println("登录成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("账号或密码错误");
}
System.out.println("是否认证成功:"+subject.isAuthenticated());
//退出登录
subject.logout();
System.out.println("是否认证成功:"+subject.isAuthenticated());
}
}
2.2 授权
一定是在认证成功后才会执行的代码。
ini文件:
[users]
#账号=密码,角色
admin=123456,admin
ldh=123456
[roles]---权限
#角色=权限
admin=user:query,user:delete,user:save,user:update
role1=user:query,user:export
package test;
public class test02 {
public static void main(String[] args) {
//创建SecurityManager对象。
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//为SecurityManager指定Realm对象,这里要使用classpath指定编译后的资源文件
IniRealm realm=new IniRealm("classpath:shiro.ini");
//通过SecurityUtil把SecurityManager设置上下文可用。生效问题。
SecurityUtils.setSecurityManager(securityManager);
//获取Subject对象
Subject subject = SecurityUtils.getSubject();
//完成认证功能AuthenticationToken:该类把账号和密码封装到该类中
UsernamePasswordToken token=new UsernamePasswordToken("admin","123456");
try {
subject.login(token);
System.out.println("登录成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("账号或密码错误");
}
//判断当前用户是否具有某个权限
boolean permitted = subject.isPermitted("user:query");
System.out.println("判断当前用户是否具有User:query权限:" + permitted);
//判断当前用户是否同时具有权限,与关系
boolean permittedAll = subject.isPermittedAll("user:query", "user:save");
System.out.println("判断当前用户是否同时具有user:query和user:save权限:"+permittedAll);
//只要有一个权限就输出一个true,或关系
boolean[] permitted1 = subject.isPermitted("user:query", "user:export");
System.out.println(Arrays.toString(permitted1));
}
}
上面写的代码中所有的数据都是基于ini文件中得来,实际开发中,数据的来源应该是从数据库中拿到的。这时我们可以自定义realm,可以帮我们完成相应的认证功能。
3.自定义realm请求
3.1 认证方法
(1)User实体类
package entity;
@Data
public class User {
private Integer id;
private String username;
private String password;
private String realname;
}
(2)service代码
package service;
public class UserService {
public User findByUsername(String username) {
if("admin".equals(username)) {
return new User(1,username,"123456","管理员");
} else if ("YKQ".equals(username)) {
return new User(2, username, "123456", "ykq");
} else {
return null;
}
}
}
(3)创建一个类并继承AuthorizingRealm
package realm;
public class MyRealm extends AuthorizingRealm {
private UserService userService = new UserService();
//授权时执行该方法
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权方法===");
return null;
}
//认证时执行该方法
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{
System.out.println("认证方法===");
//获取登录者的账号
String username = authenticationToken.getPrincipal.toString();
//根据账号查询数据库信息
User user = userService.findByUsername(username);
if(user != null) {
//Object principal,数据库中的账号
//Object credentials,密码
//String realmName,realmName名称,随便起
//密码的比对交给了 SimpleAuthenticationInfo 这个类
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(username, user.getPassword, this.getName);
return info;
}
return null;
}
}
(4)修改securityManager的realm对象
package test;
public class test03 {
public static void main(String[] args) {
//创建SecurityManager对象。
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//为SecurityManager指定Realm对象,这里要使用classpath指定编译后的资源文件
MyRealm realm=new MyRealm();
securityManager.setRealm(realm);
//通过SecurityUtil把SecurityManager设置上下文可用。生效问题。
SecurityUtils.setSecurityManager(securityManager);
//获取Subject对象
Subject subject = SecurityUtils.getSubject();
//完成认证功能AuthenticationToken:该类把账号和密码封装到该类中
UsernamePasswordToken token=new UsernamePasswordToken("admin","123456");
try {
subject.login(token);
System.out.println("登录成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("账号或密码错误");
}
//判断当前用户是否具有某个权限
boolean permitted = subject.isPermitted("user:query");
System.out.println("判断当前用户是否具有User:query权限:" + permitted);
//判断当前用户是否同时具有权限,与关系
boolean permittedAll = subject.isPermittedAll("user:query", "user:save");
System.out.println("判断当前用户是否同时具有user:query和user:save权限:"+permittedAll);
//只要有一个权限就输出一个true,或关系
boolean[] permitted1 = subject.isPermitted("user:query", "user:export");
System.out.println(Arrays.toString(permitted1));
}
}
3.2 完成自定义授权
package realm;
public class MyRealm extends AuthorizingRealm {
private UserService userService = new UserService();
//只有登录者登录成功时才会执行,授权时执行该方法
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权方法===");
//获取当前登录者的账号
String username = (String)principalCollection.getPrimaryPrincipal();
//根据账号查询当前账号具有的权限
List<String> permissions = userService.findPermissionByUsername(username);
if(permissions.size() > 0){
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissions);
return info;
}
return null;
}
//认证时执行该方法
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{
System.out.println("认证方法===");
//获取登录者的账号
String username = authenticationToken.getPrincipal.toString();
//根据账号查询数据库信息
User user = userService.findByUsername(username);
if(user != null) {
//Object principal,数据库中的账号
//Object credentials,密码
//String realmName,realmName名称,随便起
//密码的比对交给了 SimpleAuthenticationInfo 这个类
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(username, user.getPassword, this.getName);
return info;
}
return null;
}
}
package service;
public class UserService {
public User findByUsername(String username) {
if("admin".equals(username)) {
return new User(1,username,"123456","管理员");
} else if ("YKQ".equals(username)) {
return new User(2, username, "123456", "ykq");
} else {
return null;
}
}
public List<String> findPermissionByUsername(String username) {
List<String> list=new ArrayList<String>();
if("admin".equals(username)){
list.add("user:query");
list.add("user:save");
list.add("user:delete");
list.add("user:update");l
} else if ("YKQ".equals(username)){
list.add("user:query");
list.add("user:export");
}
return list;
}
}
4 密码加密
4.1 加密演示
shiro默认采用md5加密,我们可以写一个单元测试来体现这一点。
public class Test04 {
public static void main(String[] args) {
//Md5Hash中的参数表示要加密的明文
Md5Hash md5Hash = new Md5Hash("123456");
//输出加密后的密文
System.out.println(md5Hash);
}
}
解密网址会把常见的加密的内容搜集起来---造成了数据不安全。我们可以使用加盐加密来避免这一点。
public class Test04 {
public static void main(String[] args) {
//加盐加密,第二个参数就是 盐值 ,这个盐不要泄露
Md5Hash md5Hash1=new Md5Hash("123456", "aaa");
//输出加密后的密文
System.out.println(md5Hash);
}
}
我们还可以使用加盐加密,同时设置加密次数
public class Test04 {
public static void main(String[] args) {
//加盐加密,同时设置加密次数, 第三个参数就是 加密次数
Md5Hash md5Hash1=new Md5Hash("123456", "aaa", 1024);
//输出加密后的密文
System.out.println(md5Hash1);
}
}
4.2 shiro中使用md5加密
(1)设置realm加密器
package test;
public class test05 {
public static void main(String[] args) {
//创建SecurityManager对象。
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//为SecurityManager指定Realm对象,这里要使用classpath指定编译后的资源文件
MyRealm realm=new MyRealm();
//创建Hash加密器
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("MD5");//设置加密方式
MD5matcher.setHashIterations(1024);//散列的次数1024
myRealm.setcredentialsMatcher(matcher);//设置realm的密码匹配器
securityManager.setRealm(realm);
//通过SecurityUtil把SecurityManager设置上下文可用。生效问题。
SecurityUtils.setSecurityManager(securityManager);
//获取Subject对象
Subject subject = SecurityUtils.getSubject();
//完成认证功能AuthenticationToken:该类把账号和密码封装到该类中
UsernamePasswordToken token=new UsernamePasswordToken("admin","123456");
try {
subject.login(token);
System.out.println("登录成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("账号或密码错误");
}
//判断当前用户是否具有某个权限
boolean permitted = subject.isPermitted("user:query");
System.out.println("判断当前用户是否具有User:query权限:" + permitted);
//判断当前用户是否同时具有权限,与关系
boolean permittedAll = subject.isPermittedAll("user:query", "user:save");
System.out.println("判断当前用户是否同时具有user:query和user:save权限:"+permittedAll);
//只要有一个权限就输出一个true,或关系
boolean[] permitted1 = subject.isPermitted("user:query", "user:export");
System.out.println(Arrays.toString(permitted1));
}
}
在上面的创建Hash加密器中,我们并没有使用加盐值的方式。原因是在这里设置不合适,如果在这里设置盐值的话,就不能再改变盐值了。应该每个账号都应该有自己的盐值。
我们可以再realm中设置盐值。
(2)修改自定义的realm参数(多了盐值)
package realm;
public class MyRealm extends AuthorizingRealm {
private UserService userService = new UserService();
//只有登录者登录成功时才会执行,授权时执行该方法
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权方法===");
//获取当前登录者的账号
String username = (String)principalCollection.getPrimaryPrincipal();
//根据账号查询当前账号具有的权限
List<String> permissions = userService.findPermissionByUsername(username);
if(permissions.size() > 0){
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissions);
return info;
}
return null;
}
//认证时执行该方法
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{
System.out.println("认证方法===");
//获取登录者的账号
String username = authenticationToken.getPrincipal.toString();
//根据账号查询数据库信息
User user = userService.findByUsername(username);
if(user != null) {
//Object principal,数据库中的账号
//Object credentials,密码
//ByteSource credentialsSalt:使用的盐
//String realmName,realmName名称,随便起
//密码的比对交给了 SimpleAuthenticationInfo 这个类
//由于ByteSource是个接口 我们不可以直接new出来,我们要么new它的子类,要么通过工厂来拿到它
ByteSource salt=ByteSource.Util.bytes( string: "aaa");//盐值未来应该存在数据库中,从数据库拿到user的时候一并把盐值拿到 user.getSalt
//shiro会帮我们进行密码的比对。shiro会先对你的密码进行加密,再和数据库中的加密后的密码进行比较
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(username, user.getPassword, salt, this.getName);
return info;
}
return null;
}
}
评论