1.概述
模仿 Shiro 开发一个简单权限框架案例。
主要使用了以下几种设计模式:
- 单例模式
- 工厂模式
- 策略模式
- 责任链模式
该权限框架主要有以下开发点:
- 读取配置文件
- 密码加密(策略模式,工厂模式)
- 身份认证(责任链模式,单例模式)
- 权限认证
2.各个模块
2.1 读取配置文件
public class Config {
private static Map<String, String> configMap = new HashMap<>();
static {
InputStream in = Config.class.getResourceAsStream("/permission.ini");
DataInputStream dis = new DataInputStream(in);
String str;
try{
while ((str = dis.readLine()) != null){
String[] configs = str.split("=");
if(configs.length == 2){
configMap.put(configs[0].trim(),configs[1].trim());
}
}
dis.close();
}catch (Exception e){
throw new RuntimeException("配置文件不存在");
}
}
public static String get(String name){
return configMap.get(name);
}
public static String get(String name,String defaultValue){
String value = configMap.get(name);
return value == null ? defaultValue : value;
}
}
在上面 Config 类中用到了 IO 流,IO 流本身就是装饰器模式的一种应用。
2.2 密码加密
密码加密模式采用 MD5 加密,但是我们要开发一个可灵活扩展的框架,允许开发者们自定义加密方式,并且能够通过修改配置文件来修改加密方式。这里我们采用了策略模式,其类图如下:
public interface PasswordEncrypt {
String encrypt(String password);
}
默认的 MD5 加密:
public class Md5Encrypt implements PasswordEncrypt {
@Override
public String encrypt(String password) {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
md5.update(password.getBytes());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return new BigInteger(1,md5.digest()).toString(16);
}
}
用工厂来创建加密策略类(使用反射机制动态创建策略类):
public class EncryptFactory {
/**
* md5
* @param clazz 类名
* @return
*/
public static PasswordEncrypt create(String clazz){
try {
Class cls = Class.forName(clazz);
Object obj = cls.newInstance();
if(obj instanceof PasswordEncrypt){
return (PasswordEncrypt)obj;
}else{
throw new RuntimeException("class not found:" + clazz);
}
} catch (Exception e) {
throw new RuntimeException("class not found:" + clazz);
}
}
}
在 EncryptContext 中,根据配置文件的配置来动态选择使用哪种加密策略,默认 MD5 加密
public class EncryptContext {
private PasswordEncrypt pe;
public EncryptContext() {
String cls = Config.get("encryptType","com.design.pattern.encrypt.Md5Encrypt");
this.pe = EncryptFactory.create(cls);
}
public String encrypt(String password){
return this.pe.encrypt(password);
}
}
测试一下默认的 MD5 加密:
String encryptedPwd = (new EncryptContext()).encrypt("123");
System.out.println("加密后:"+encryptedPwd);
2.2.1 自定义加密逻辑
第一,增加加密策略类:
public class MyEncrypt implements PasswordEncrypt {
@Override
public String encrypt(String password) {
return password + " encrypted pwd";
}
}
第二,把该类配置到配置文件中:
encryptType = test.encrypt.MyEncrypt
2.3 身份认证与权限认证
Realm 这个概念来自于 Shiro 框架,它是用于进行身份认证和获取用户权限。在本案例中,Realm 主要有两个抽象方法:
- abstract boolean loginAuth(AuthToken token);
- abstract PermissionInfo doGetPermissionInfo(AuthToken token);
第一个方法用于判断当前用户是否认证成功,在用户登录时将调用该方法。
第二个方法是获取当前用户拥有哪些权限,在判断用户是否有某权限时调用该方法。
这两个方法都需要开发者去实现。
开发者可以自定义多个 Realm,比如 Realm1 验证用户名密码,Realm2 用于验证第三方登录(微信登录等)。在这里我使用了责任链模式,多个 Realm 只要有一个验证通过,那么该用户就登录成功。
多个自定义 Realm 将形成一个责任链,而形成责任链的步骤将由AuthManager 完成,并且AuthManager 类是一个单例模式。
AuthRealm类:
public abstract class AuthRealm {
private AuthRealm successor;
public void setSuccessor(AuthRealm realm){
this.successor = realm;
}
public final boolean auth(AuthToken token){
if(token == null) return false;
//如果验证成功,就返回成功
if(this.loginAuth(token)){
return true;
}
//失败就将请求传给下一个责任处理器
return successor != null && successor.auth(token);
}
/**
* 登录验证
* @return
*/
protected abstract boolean loginAuth(AuthToken token);
/**
* 权限验证
* @return
*/
protected abstract PermissionInfo doGetPermissionInfo(AuthToken token);
}
在上面类中,使用到了AuthToken 和 PermissionInfo ,前者是用户认证信息,存放用户名密码等,后者是保存权限信息,包括“角色”和“权限”。
AuthToken类:
public class AuthToken {
private String username;
private String password;
public AuthToken() {
}
public AuthToken(String username, String password) {
this.username = username;
this.password = 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;
}
}
PermissionInfo 类:
public class PermissionInfo {
private Set<String> permissions;
private Set<String> roles;
public Set<String> getPermissions() {
return permissions;
}
public void setPermissions(Set<String> permissions) {
this.permissions = permissions;
}
public Set<String> getRoles() {
return roles;
}
public void setRoles(Set<String> roles) {
this.roles = roles;
}
/**
* 判断是否有某权限
* @param permission
* @return
*/
public boolean isPermitted(String permission){
return this.permissions.contains(permission);
}
}
AuthManager类:
public class AuthManager {
private List<AuthRealm> list = new ArrayList<>();
private static AuthManager instance = new AuthManager();
/**
* 私有构造方法,读取配置文件,通过反射机制生成Realm,并构建责任链
*/
private AuthManager() {
String realms = Config.get("realms");
if(realms == null || realms.isEmpty()){
throw new RuntimeException("请定义Realm");
}
String[] clss = realms.split(",");
for (int i = 0;i < clss.length; i++){
try {
Object obj = Class.forName(clss[i]).newInstance();
if(obj instanceof AuthRealm){
this.list.add((AuthRealm)obj);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//形成责任链
for (int i=0;i<list.size()-1;i++){
AuthRealm next = list.get(i+1);
if(next != null){
list.get(i).setSuccessor(next);
}
}
}
/**
* 调用 Realm 中的 DoGetPermissionInfo 方法,如果有多个 Realm,只调用第一个
* @param token
* @return
*/
public PermissionInfo getPermissionInfo(AuthToken token){
if(token == null){
return null;
}
if(list.size() > 0){
return this.list.get(0).doGetPermissionInfo(token);
}
return null;
}
/**
* 登录认证,调用 Realm 责任链
* @return
*/
public boolean auth(AuthToken token){
if(list.size() == 0){
return false;
}
return list.get(0).auth(token);
}
/**
* 单例
* @return
*/
public static AuthManager getInstance(){
return instance;
}
}
AuthManager 主要有以下职责:
- 生成 Realm 责任链
- 调用身份认证和权限认证方法
2.4 主体类
还缺一个用户主体类Subject。所谓的用户主体,有点类似 Web 开发中的 Session,一个用户请求对应一个Session,而在权限框架中,用户主体类 Subject 就代表了当前用户。
Auth 接口,定义了登录,权限等方法:
public interface Auth {
/**
* 登录操作
* @param token
* @return
*/
boolean login(AuthToken token);
/**
* 是否已登录
* @return
*/
boolean isLogin();
/**
* 是否有权限
* @param permission
* @return
*/
boolean isPermitted(String permission);
}
用户主体类 Subject :
/**
* 登录用户主体
*/
public class Subject implements Auth{
private AuthToken token;
@Override
public boolean login(AuthToken token) {
//调用密码加密策略
String password = (new EncryptContext()).encrypt(token.getPassword());
token.setPassword(password);
//调用auth方法,即触发责任链
if(AuthManager.getInstance().auth(token)){
System.out.println("登录成功");
this.token = token;
return true;
}
return false;
}
@Override
public boolean isLogin() {
return token != null;
}
@Override
public boolean isPermitted(String permission) {
PermissionInfo info = AuthManager.getInstance().getPermissionInfo(this.token);
return info != null && info.isPermitted(permission);
}
public String getUsername(){
return token.getUsername();
}
}
工具类 SecurityUtils,提供了全局获取用户主体类 Subject 的方法:
public class SecurityUtils {
private static Map<String, Subject> subjectList = new HashMap<>();
/**
* 获取当前请求的用户
* @return
*/
public static Subject getSubject(){
//此处应借用 Session 等方式获取当前请求用户
String name = "123";
Subject subject = subjectList.get(name);
return subject == null ? new Subject() : subject;
}
public static void addSubject(Subject subject){
subjectList.put("123",subject);
}
}
实际上上面SecurityUtils 中的 getSubject() 的实现机制也应该是一个类似 Session 的机制,就像我们在 Web 请求中获取当前Session,Session 和当前用户对应。但本次案例主要是介绍设计模式,就不去实现那么复杂的功能了,因此这里就简单地直接给出 Subject 了。
2.5 整体结构图
2.6 测试
创建 Realm1:
public class Realm1 extends AuthRealm {
@Override
protected boolean loginAuth(AuthToken token) {
System.out.println("===Realm1 loginAuth===");
String username = token.getUsername();
String pwd = token.getPassword();
//传进来的密码是加密过的密码,直接和数据库中的密码比对
System.out.println("pwd:"+pwd);
//查询数据库操作略过
return false;
}
@Override
protected PermissionInfo doGetPermissionInfo(AuthToken token) {
String username = token.getUsername();
System.out.println("doGetPermissionInfo1");
//从数据库读取该用户的权限信息
PermissionInfo info = new PermissionInfo();
Set<String> s = new HashSet<String>();
s.add("permission1");
s.add("permission2");
info.setPermissions(s);
//角色
Set<String> r = new HashSet<String>();
r.add("role1");
info.setRoles(r);
return info;
}
}
再创建一个 Realm2,Realm2 的和1结构是一样的,具体的业务逻辑要根据你项目实际情况去修改,这里只是测试,就直接给出一模一样的代码:
public class Realm2 extends AuthRealm {
@Override
protected boolean loginAuth(AuthToken token) {
System.out.println("===Realm2 loginAuth===");
String username = token.getUsername();
String pwd = token.getPassword();
//传进来的密码是加密过的密码,直接和数据库中的密码比对
System.out.println("pwd:"+pwd);
//查询数据库操作略过
return true;
}
@Override
protected PermissionInfo doGetPermissionInfo(AuthToken token) {
String username = token.getUsername();
System.out.println("doGetPermissionInfo2");
//从数据库读取该用户的权限信息
PermissionInfo info = new PermissionInfo();
Set<String> s = new HashSet<String>();
s.add("printer:print");
s.add("printer:query");
info.setPermissions(s);
//角色
Set<String> r = new HashSet<String>();
r.add("role1");
info.setRoles(r);
return info;
}
}
然后将两个 Realm 配置到配置文件中,多个 Realm 用逗号隔开:
encryptType = test.encrypt.MyEncrypt
realms=test.realm.Realm1,test.realm.Realm2
测试代码:
public class TestDemo {
public static void main(String[] args) throws IOException{
//测试密码加密
String encryptedPwd = (new EncryptContext()).encrypt("123");
System.out.println("加密后:"+encryptedPwd);
//获取当前用户
Subject currentUser = SecurityUtils.getSubject();
//是否登录
System.out.println("是否已登录:"+currentUser.isLogin());
//执行登录操作
currentUser.login(new AuthToken("admin","123"));
//是否登录
System.out.println("是否已登录:"+currentUser.isLogin());
//是否有权限,权限用字符串表示
System.out.println("是否有权限:"+currentUser.isPermitted("permission1"));
}
}
测试结果:
3.总结
仅仅看是没用的,看10遍真的不如自己写一遍。找一个知名的开源框架,了解其工作流程后,尝试着用设计模式自己去写一个简单的例子,例如写个 SpringMVC。就像本节课的权限框架的例子,就是仿照 Shiro 框架写的。