安全應該是互聯網公司的一道生命線,幾乎任何的公司都會涉及到這方面的需求。在Java領域一般有Spring Security、Apache Shiro等安全框架,但是由于Spring Security過于龐大和復雜,大多數公司會選擇Apache Shiro來使用,這篇文章會先介紹一下Apache Shiro,在結合Spring Boot給出使用案例。
Apache Shiro簡介
Apache Shiro是一個功能強大、靈活的,開源的安全框架。它可以干凈利落地處理身份驗證、授權、企業會話管理和加密。
Shiro的主要功能
- 驗證用戶身份
- 用戶訪問權限控制,比如:判斷用戶是否分配了一定的安全角色、判斷用戶是否被授予完成某個操作的權限
- 在非 web 或 EJB 容器的環境下可以任意使用Session API
- 可以響應認證、訪問控制,或者 Session 生命周期中發生的事件
- 可將一個或以上用戶安全數據源數據組合成一個復合的用戶 “view”(視圖)
- 支持單點登錄(SSO)功能
- 支持提供“Remember Me”服務,獲取用戶關聯信息而無需登錄
Apache Shiro Features 特性
Apache Shiro是一個全面的、蘊含豐富功能的安全框架。Shiro的功能框架圖:
Authentication(認證), Authorization(授權), Session Management(會話管理), Cryptography(加密)被 Shiro 框架的開發團隊稱之為應用安全的四大基石。
- Authentication:用戶身份識別,通常被稱為用戶“登錄”
- Authorization:訪問控制。比如某個用戶是否具有某個操作的使用權限。
- Session Management:特定于用戶的會話管理,甚至在非web 或 EJB 應用程序。
- Cryptography:在對數據源使用加密算法加密的同時,保證易于使用。
還有其他的功能來支持和加強這些不同應用環境下安全領域的關注點。特別是對以下的功能支持:
- Web支持:Shiro 提供的 web 支持 api ,可以很輕松的保護 web 應用程序的安全。
- 緩存:緩存是 Apache Shiro 保證安全操作快速、高效的重要手段。
- 并發:Apache Shiro 支持多線程應用程序的并發特性。
- 測試:支持單元測試和集成測試,確保代碼和預想的一樣安全。
- “Run As”:這個功能允許用戶假設另一個用戶的身份(在許可的前提下)。
- “Remember Me”:跨 session 記錄用戶的身份,只有在強制需要時才需要登錄。
注意: Shiro不會去維護用戶、維護權限,這些需要我們自己去設計/提供,然后通過相應的接口注入給Shiro。
High-Level Overview 高級概述
在概念層,Shiro 架構包含三個主要的理念:Subject、SecurityManager和 Realm。下圖展示了這些組件如何相互作用:
- Subject:當前用戶,Subject 可以是一個人,但也可以是第三方服務、守護進程帳戶、時鐘守護任務或者其它–當前和軟件交互的任何事件。
- SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架構的核心,配合內部安全組件共同組成安全傘。
- Realms:用于進行權限信息的驗證,我們自己實現。Realm 本質上是一個特定的安全 DAO:它封裝與數據源連接的細節,得到Shiro 所需的相關的數據。在配置 Shiro 的時候,你必須指定至少一個Realm 來實現認證(authentication)和/或授權(authorization)。
我們需要實現Realms的Authentication 和 Authorization。其中 Authentication 是用來驗證用戶身份,Authorization 是授權訪問控制,用于對用戶進行的操作授權,證明該用戶是否允許進行當前操作,如訪問某個鏈接,某個資源文件等。
構建項目
添加shiro-spring依賴*
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
配置文件
spring:
datasource:
url: jdbc:mysql://localhost:3306/login
username: root
password: 123456
#schema: database/import.sql
#sql-script-encoding: utf-8
driver-class-name: com.mysql.jdbc.Driver
jpa:
database: mysql
show-sql: true
hibernate:
ddl-auto: update
naming:
strategy: org.hibernate.cfg.DefaultComponentSafeNamingStrategy
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5Dialect
thymeleaf:
cache: false
mode: LEGACYHTML5
RBAC
RBAC 是基于角色的訪問控制(Role-Based Access Control )在 RBAC 中,權限與角色相關聯,用戶通過成為適當角色的成員而得到這些角色的權限。這就極大地簡化了權限的管理。這樣管理都是層級相互依賴的,權限賦予給角色,而把角色又賦予用戶,這樣的權限設計很清楚,管理起來很方便。
用戶信息
@Entity
public class UserInfo implements Serializable {
@Id
@GeneratedValue
private Integer uid;
@Column(unique =true)
private String username;//帳號
private String name;//名稱(昵稱或者真實姓名,不同系統不同定義)
private String password; //密碼;
private String salt;//加密密碼的鹽
private byte state;//用戶狀態,0:創建未認證(比如沒有激活,沒有輸入驗證碼等等)--等待驗證的用戶 , 1:正常狀態,2:用戶被鎖定.
@ManyToMany(fetch= FetchType.EAGER)//立即從數據庫中進行加載數據;
@JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "uid") }, inverseJoinColumns ={@JoinColumn(name = "roleId") })
private List<SysRole> roleList;// 一個用戶具有多個角色
/**
* 密碼鹽,重新對鹽重新進行了定義,用戶名+salt,這樣就更加不容易被破解
*/
public String getCredentialsSalt(){
return this.username+this.salt;
}
...省略set/get方法
角色信息
@Entity
public class SysRole {
@Id@GeneratedValue
private Integer id; // 編號
private String role; // 角色標識程序中判斷使用,如"admin",這個是唯一的:
private String description; // 角色描述,UI界面顯示使用
private Boolean available = Boolean.FALSE; // 是否可用,如果不可用將不會添加給用戶
//角色 -- 權限關系:多對多關系;
@ManyToMany(fetch= FetchType.EAGER)
@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")})
private List<SysPermission> permissions;
// 用戶 - 角色關系定義;
@ManyToMany
@JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="uid")})
private List<UserInfo> userInfos;// 一個角色對應多個用戶
權限信息
@Entity
public class SysPermission implements Serializable {
@Id@GeneratedValue
private Integer id;//主鍵.
private String name;//名稱.
@Column(columnDefinition="enum('menu','button')")
private String resourceType;//資源類型,[menu|button]
private String url;//資源路徑.
private String permission; //權限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
private Long parentId; //父編號
private String parentIds; //父編號列表
private Boolean available = Boolean.FALSE;
@ManyToMany
@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")})
private List<SysRole> roles;
根據以上的代碼會自動生成user_info(用戶信息表)、sys_role(角色表)、sys_permission(權限表)、sys_user_role(用戶角色表)、sys_role_permission(角色權限表)這五張表,為了方便測試我們給這五張表插入一些初始化數據:
INSERT INTO `user_info` (`uid`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', '管理員', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0);
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (1,0,'用戶管理',0,'0/','userInfo:view','menu','userInfo/userList');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (2,0,'用戶添加',1,'0/1','userInfo:add','button','userInfo/userAdd');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (3,0,'用戶刪除',1,'0/1','userInfo:del','button','userInfo/userDel');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (1,0,'管理員','admin');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (2,0,'VIP會員','vip');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (3,1,'test','test');
INSERT INTO `sys_role_permission` VALUES ('1', '1');
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (3,2);
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);
Shiro 配置
首先要配置的是ShiroConfig類,Apache Shiro 核心通過 Filter 來實現,就好像SpringMvc 通過DispachServlet 來主控制一樣。 既然是使用 Filter 一般也就能猜到,是通過URL規則來進行過濾和權限校驗,所以我們需要定義一系列關于URL的規則和訪問權限。
ShiroConfig
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//攔截器.
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
// 配置不會被攔截的鏈接 順序判斷
filterChainDefinitionMap.put("/static/**", "anon");
//配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了
filterChainDefinitionMap.put("/logout", "logout");
//<!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最為下邊 -->:這是一個坑呢,一不小心代碼就不好使了;
//<!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問-->
filterChainDefinitionMap.put("/**", "authc");
// 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登錄成功后要跳轉的鏈接
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授權界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 憑證匹配器(由于我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了 )
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次數,比如散列兩次,相當于 md5(md5(""));
return hashedCredentialsMatcher;
}
@Bean
public MyShiroRealm myShiroRealm(){
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
/**
* 開啟shiro aop注解支持.
* 使用代理方式;所以需要開啟代碼支持;
* @param securityManager
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean(name="simpleMappingExceptionResolver")
public SimpleMappingExceptionResolver
createSimpleMappingExceptionResolver() {
SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("DatabaseException", "databaseError");//數據庫異常處理
mappings.setProperty("UnauthorizedException","403");
r.setExceptionMappings(mappings); // None by default
r.setDefaultErrorView("error"); // No default
r.setExceptionAttribute("ex"); // Default is "exception"
//r.setWarnLogCategory("example.MvcLogger"); // No default
return r;
}
}
Filter Chain定義說明:
- 1、一個URL可以配置多個Filter,使用逗號分隔
- 2、當設置多個過濾器時,全部驗證通過,才視為通過
- 3、部分過濾器可指定參數,如perms,roles
Shiro內置的FilterChain
- anon:所有url都都可以匿名訪問
- authc: 需要認證才能進行訪問
- user:配置記住我或認證通過可以訪問
......
登錄認證實現
在認證、授權內部實現機制中都有提到,最終處理都將交給Real進行處理。因為在Shiro中,最終是通過Realm來獲取應用程序中的用戶、角色及權限信息的。通常情況下,在Realm中會直接從我們的數據源中獲取Shiro需要的驗證信息。可以說,Realm是專用于安全框架的DAO. Shiro的認證過程最終會交由Realm執行,這時會調用Realm的getAuthenticationInfo(token)方法。
該方法主要執行以下操作:
- 1、檢查提交的進行認證的令牌信息
- 2、根據令牌信息從數據源(通常為數據庫)中獲取用戶信息
- 3、對用戶信息進行匹配驗證。
- 4、驗證通過將返回一個封裝了用戶信息的AuthenticationInfo實例。
- 5、驗證失敗則拋出AuthenticationException異常信息。
而在我們的應用程序中要做的就是自定義一個Realm類,繼承AuthorizingRealm抽象類,重載doGetAuthenticationInfo(),重寫獲取用戶信息的方法。
鏈接權限的實現
shiro的權限授權是通過繼承AuthorizingRealm抽象類,重載doGetAuthorizationInfo();當訪問到頁面的時候,鏈接配置了相應的權限或者shiro標簽才會執行此方法否則不會執行,所以如果只是簡單的身份認證沒有權限的控制的話,那么這個方法可以不進行實現,直接返回null即可。在這個方法中主要是使用類:SimpleAuthorizationInfo進行角色的添加和權限的添加。
public class MyShiroRealm extends AuthorizingRealm {
@Resource
private UserInfoService userInfoService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("權限配置-->MyShiroRealm.doGetAuthorizationInfo()");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
UserInfo userInfo = (UserInfo)principals.getPrimaryPrincipal();
for(SysRole role:userInfo.getRoleList()){
authorizationInfo.addRole(role.getRole());
for(SysPermission p:role.getPermissions()){
authorizationInfo.addStringPermission(p.getPermission());
}
}
return authorizationInfo;
}
doGetAuthenticationInfo的重寫
//主要是用來進行身份認證的,也就是說驗證用戶輸入的賬號和密碼是否正確。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
//獲取用戶的輸入的賬號.
String username = (String)token.getPrincipal();
System.out.println(token.getCredentials());
//通過username從數據庫中查找 User對象,如果找到,沒找到.
//實際項目中,這里可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重復執行該方法
UserInfo userInfo = userInfoService.findByUsername(username);
System.out.println("----->>userInfo="+userInfo);
if(userInfo == null){
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo, //用戶名
userInfo.getPassword(), //密碼
ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt
getName() //realm name
);
return authenticationInfo;
}
}
當然也可以添加set集合:roles是從數據庫查詢的當前用戶的角色,stringPermissions是從數據庫查詢的當前用戶對應的權限
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(stringPermissions);
就是說如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “perms[權限添加]”);就說明訪問/add這個鏈接必須要有“權限添加”這個權限才可以訪問,如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “roles[100002],perms[權限添加]”);就說明訪問/add這個鏈接必須要有“權限添加”這個權限和具有“100002”這個角色才可以訪問。
登錄實現
登錄過程其實只是處理異常的相關信息,具體的登錄驗證交給shiro來處理
@Controller
public class HomeController {
@RequestMapping({"/","/index"})
public String index(){
return"/index";
}
@RequestMapping("/login")
public String login(HttpServletRequest request, Map<String, Object> map) throws Exception{
System.out.println("HomeController.login()");
// 登錄失敗從request中獲取shiro處理的異常信息。
// shiroLoginFailure:就是shiro異常類的全類名.
String exception = (String) request.getAttribute("shiroLoginFailure");
System.out.println("exception=" + exception);
String msg = "";
if (exception != null) {
if (UnknownAccountException.class.getName().equals(exception)) {
System.out.println("UnknownAccountException -- > 賬號不存在:");
msg = "UnknownAccountException -- > 賬號不存在:";
} else if (IncorrectCredentialsException.class.getName().equals(exception)) {
System.out.println("IncorrectCredentialsException -- > 密碼不正確:");
msg = "IncorrectCredentialsException -- > 密碼不正確:";
} else if ("kaptchaValidateFailed".equals(exception)) {
System.out.println("kaptchaValidateFailed -- > 驗證碼錯誤");
msg = "kaptchaValidateFailed -- > 驗證碼錯誤";
} else {
msg = "else >> "+exception;
System.out.println("else -- >" + exception);
}
}
map.put("msg", msg);
// 此方法不處理登錄成功,由shiro進行處理
return "/login";
}
@RequestMapping("/403")
public String unauthorizedRole(){
System.out.println("------沒有權限-------");
return "403";
}
}
dao層
public interface UserInfoDao extends CrudRepository<UserInfo,Long> {
/**通過username查找用戶信息;*/
public UserInfo findByUsername(String username);
}
service層
public interface UserInfoService {
/**通過username查找用戶信息;*/
public UserInfo findByUsername(String username);
}
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Resource
private UserInfoDao userInfoDao;
@Override
public UserInfo findByUsername(String username) {
System.out.println("UserInfoServiceImpl.findByUsername()");
return userInfoDao.findByUsername(username);
}
}
測試
1、編寫好后就可以啟動程序,訪問http://localhost:8080/userInfo/userList頁面,由于沒有登錄就會跳轉到http://localhost:8080/login頁面。登錄之后就會跳轉到index頁面,登錄后,直接在瀏覽器中輸入http://localhost:8080/userInfo/userList訪問就會看到用戶信息。上面這些操作時候觸發MyShiroRealm.doGetAuthenticationInfo()這個方法,也就是登錄認證的方法。
2、登錄admin賬戶,訪問:http://127.0.0.1:8080/userInfo/userAdd顯示用戶添加界面,訪問http://127.0.0.1:8080/userInfo/userDel顯示403沒有權限。上面這些操作時候觸發MyShiroRealm.doGetAuthorizationInfo()這個方面,也就是權限校驗的方法。
3、修改admin不同的權限進行測試
shiro很強大,這僅僅是完成了登錄認證和權限管理這兩個功能,更多內容以后有時間再做探討。