java乾貨系列—權限框架shiro


shiroConfig 也不復雜,基本就三個方法。再說這三個方法之前,我想給大家說一下shiro的三個核心概念:

Subject: 代表當前正在執行操作的用戶,但Subject代表的可以是人,也可以是任何第三方系統帳號。當然每個subject實例都會被綁定到SercurityManger上。

SecurityManger:SecurityManager是Shiro核心,主要協調Shiro內部的各種安全組件,這個我們不需要太關注,只需要知道可以設置自定的Realm。

Realm:用戶數據和Shiro數據交互的橋樑。比如需要用戶身份認證、權限認證。都是需要通過Realm來讀取數據。

一、maven依賴

<code><dependency>
<groupid>org.apache.shiro/<groupid>
<artifactid>shiro-spring/<artifactid>
<version>1.4.0/<version>
/<dependency>
************************************************
<dependency>
<groupid>org.apache.shiro/<groupid>
<artifactid>shiro-ehcache/<artifactid>
<version>1.2.2/<version>
/<dependency>/<code>

備註:緩存先用ehcache 後續會添加一欄redis

二、配置Realms

shiroConfig核心是通過Filter來實現的,主要通過url規則來進行過濾和權限效驗

<code>public class MyShiroRealm extends AuthorizingRealm {

@Resource
private UserService userService;

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
User user =(User) principals.getPrimaryPrincipal();

for(Role role:user.getRoleList()){
authorizationInfo.addRole(role.getRoleName());

for (Permissions permissions : role.getPermissionsList()){
authorizationInfo.addStringPermission(permissions.getPerName());
}
}

// 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;
}

/*主要是用來進行身份認證的,也就是說驗證用戶輸入的賬號和密碼是否正確。*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {

//獲取用戶的輸入的賬號.
String username = (String)token.getPrincipal();
System.out.println(token.getCredentials());
//通過username從數據庫中查找 User對象,如果找到,沒找到.
//實際項目中,這裡可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
User userInfo = userService.getUserByLoginName(username);
if(userInfo == null){
return null;
}
//這裡設置了用戶狀態為1的話為鎖定

if(userInfo.getStatus() == 1) {
throw new XbaseException(XbaseError.LOGIN_USER_PROHIBIT);
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo, //用戶名
userInfo.getPassword(), //密碼
ByteSource.Util.bytes(userInfo.getSalt()),//salt=username+salt
getName() //realm name
);
return authenticationInfo;
}

}
/<code>

三、配置ShiroConfig類

shiroConfig核心是通過Filter來實現的,主要通過url規則來進行過濾和權限效驗,

<code>@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {

ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//攔截器.
Map<string> filterChainDefinitionMap = new LinkedHashMap<string>();
// 配置不會被攔截的鏈接 順序判斷
// 配置靜態文件不攔截
filterChainDefinitionMap.put("/static/**", "anon");
// 公共文件不攔截
filterChainDefinitionMap.put("/public/**","anon");
//配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了
filterChainDefinitionMap.put("/logout", "logout");
//:這是一個坑呢,一不小心代碼就不好使了;

//
filterChainDefinitionMap.put("/**", "anon");
// 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登錄成功後要跳轉的鏈接
shiroFilterFactoryBean.setSuccessUrl("/index");

//未授權界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}

/**
* 自定義sessionManager
* @return
*/
@Bean
public SessionManager sessionManager(){
ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
//這裡可以不設置。Shiro有默認的session管理。如果緩存為Redis則需改用Redis的管理
shiroSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
return shiroSessionManager;
}

/**
* 憑證匹配器
* (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了
* )
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這裡使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次數,比如散列兩次,相當於 md5(md5(""));

return hashedCredentialsMatcher;
}
/**
* shiro 用戶數據注入
* @return
*/
@Bean
public MyShiroRealm myShiroRealm(){
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}

/**
* 配置管理層。即安全控制層
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
//自定義session管理
securityManager.setSessionManager(sessionManager());
//自定義緩存實現
securityManager.setCacheManager(ehCacheManager());
return securityManager;
}


/**
* 開啟緩存
* shiro-ehcache實現
* @return
*/
@Bean
public EhCacheManager ehCacheManager() {
System.out.println("ShiroConfiguration.getEhCacheManager()");
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return ehCacheManager;
}
/**
* 開啟shiro aop註解支持.
* 使用代理方式;所以需要開啟代碼支持;
* @param securityManager

* @return
*/
@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;
}
}
/<string>/<string>/<code>

四、Controller驗證用戶登錄請求

<code>public Map<string> login(String loginName, String password) {
ResultModel<boolean> result = new ResultModel<boolean>();

Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(loginName,password);
try {
subject.login(token);
result.setAddition("token", subject.getSession().getId());
result.setReturnMessage("登錄成功");
//登錄成功 將用戶信息存儲到shiro中
subject.getSession().setAttribute("userSession",subject.getPrincipal());
}catch (IncorrectCredentialsException e){
result.setReturnMessage("密碼錯誤");
}catch (LockedAccountException e){
result.setReturnMessage("登錄失敗,該用戶已被凍結");
}catch (AuthenticationException e){
result.setReturnMessage("該用戶不存在");
}catch (Exception e){

e.printStackTrace();
}
return result.dump();
}
/<boolean>/<boolean>/<string>/<code>

五、shiro全局異常

定義的filter必須滿足filter instanceof AuthorizationFilter,只有perms,roles,ssl,rest,port才是屬於AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,所以unauthorizedUrl設置後頁面不跳轉

解決方法要麼就使用perms,roles,ssl,rest,port,要麼自己配置異常處理,進行頁面跳轉。

這裡選擇自定義異常處理。處理全局異常。

<code>public class GlobalExceptionResolver implements HandlerExceptionResolver {

private static Logger logger = LoggerFactory.getLogger(GlobalExceptionResolver.class);

@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView mv;
//進行異常判斷。如果捕獲異常請求跳轉。
if(ex instanceof UnauthorizedException){
mv = new ModelAndView("/user/unauth");
return mv;
}else {
mv = new ModelAndView();
FastJsonJsonView view = new FastJsonJsonView();
BaseResponse baseResponse = new BaseResponse();
baseResponse.setMsg("服務器異常");
ex.printStackTrace();
logger.error(ExceptionUtils.getFullStackTrace(ex));
Map<string> map = new HashMap<>();
String beanString = JSON.toJSONString(baseResponse);
map = JSON.parseObject(beanString,Map.class);
view.setAttributesMap(map);
mv.setView(view);
return mv;

}

}
}
/<string>/<code>

並在shiroConfig中添加bean

<code> /**
* 註冊全局異常處理
* @return
*/
@Bean(name = "exceptionHandler")
public HandlerExceptionResolver handlerExceptionResolver(){
return new GlobalExceptionResolver();
}/<code>

六、session緩存使用

創建ehcahe-shiro.xml

<code>
<ehcache>
<diskstore>

<defaultcache> maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>

<cache> maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
/<cache>
/<defaultcache>/<ehcache>
/<code>

修改shiroConfig

<code>//添加方法
/**
* 開啟緩存
* shiro-ehcache實現
* @return
*/
@Bean
public EhCacheManager ehCacheManager() {
System.out.println("ShiroConfiguration.getEhCacheManager()");
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return ehCacheManager;
}
//修改securityManager方法。
/**
* 配置管理層。即安全控制層
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
//自定義緩存實現
securityManager.setCacheManager(ehCacheManager());
return securityManager;
}/<code>

備註 如前後端分離 採用請求帶token效驗session的方式可以採用配置如下方式

<code>public class ShiroSessionManager extends DefaultWebSessionManager { 


private static final String AUTHORIZATION = "authToken";

private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

public ShiroSessionManager(){
super();
}

@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response){
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
System.out.println("id:"+id);
if(StringUtils.isEmpty(id)){
//如果沒有攜帶id參數則按照父類的方式在cookie進行獲取
System.out.println("super:"+super.getSessionId(request, response));
return super.getSessionId(request, response);
}else{
//如果請求頭中有 authToken 則其值為sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TRUE);
return id;
}
}
}/<code>

修改一shiroConfig 將自定義的shiroSessionManager注入進去

<code>//添加bean
/**
* 自定義sessionManager
* @return
*/
@Bean
public SessionManager sessionManager(){
ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
//這裡可以不設置。Shiro有默認的session管理。如果緩存為Redis則需改用Redis的管理
shiroSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
return shiroSessionManager;
}
//修改securityManager()方法

/**
* 配置管理層。即安全控制層
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
//自定義session管理
securityManager.setSessionManager(sessionManager());
//自定義緩存實現
securityManager.setCacheManager(ehCacheManager());
return securityManager;
}/<code>

如採用單服務進行session驗證的話。

登錄成功時候

<code>public Map<string> login(String loginName, String password) {
ResultModel<boolean> result = new ResultModel<boolean>();

Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(loginName,password);
try {
subject.login(token);
result.setAddition("token", subject.getSession().getId());
result.setReturnMessage("登錄成功");
//登錄成功 將用戶信息存儲到shiro中
// 獲取方法 創建subject 進行獲取用戶信息
// Subject subject = SecurityUtils.getSubject();
// User user = (User) subject.getSession().getAttribute("userSession");
// System.out.println(user.getLoginName()+" ************ "+user.getCompany());
subject.getSession().setAttribute("userSession",subject.getPrincipal());
}catch (IncorrectCredentialsException e){
result.setReturnMessage("密碼錯誤");
}catch (LockedAccountException e){
result.setReturnMessage("登錄失敗,該用戶已被凍結");
}catch (AuthenticationException e){

result.setReturnMessage("該用戶不存在");
}catch (Exception e){
e.printStackTrace();
}
return result.dump();
}/<boolean>/<boolean>/<string>/<code>

七、註解實現shiro

Shiro共有5個註解:

RequiresAuthentication:

使用該註解標註的類,實例,方法在訪問或調用時,當前Subject必須在當前session中已經過認證。

RequiresGuest:

使用該註解標註的類,實例,方法在訪問或調用時,當前Subject可以是“gust”身份,不需要經過認證或者在原先的session中存在記錄。

RequiresPermissions:

當前Subject需要擁有某些特定的權限時,才能執行被該註解標註的方法。如果當前Subject不具有這樣的權限,則方法不會被執行。

RequiresRoles:

當前Subject必須擁有所有指定的角色時,才能訪問被該註解標註的方法。如果當天Subject不同時擁有所有指定角色,則方法不會執行還會拋出AuthorizationException異常。

RequiresUser

當前Subject必須是應用的用戶,才能訪問或調用被該註解標註的類,實例,方法。

shiro提供了SimpleHash類 進行解密。


Shiro的認證註解處理是有內定的處理順序的,如果有個多個註解的話,前面的通過了會繼續檢查後面的,若不通過則直接返回,處理順序依次為(與實際聲明順序無關):

RequiresRoles

RequiresPermissions

RequiresAuthentication

RequiresUser

RequiresGuest

例如:你同時聲明瞭RequiresRoles和RequiresPermissions,那就要求擁有此角色的同時還得擁有相應的權限。


需要注意的是,shiro註解可以放到Controller層方法上,也可以放到Service層方法上。但在日常開發中,往往會在Service層添加“@Transactional”註解,為的是當Service發送數據庫異常時,所有數據庫操作可以回滾。當在Service層添加“@Transactional”註解後,執行Service方法前,會開啟事務。此時的Service已經是一個代理對象了,此時如果我們將Shiro的權限註解加載Service層是不合適的,此時需要加到Controller層。這是因為不能讓Service是“代理的代理”,如果強行注入,會發生類型轉換異常。因此儘量加在Controller層比較好。

下面用@RequiresPermissions為例,將註解加在controller層,看在項目中是怎麼用的,其他四種都是類似的。

當方法前可能需要多個權限時,可以自定義註解。如下,就表示RequiresPermissions可以有多個權限標識,默認的邏輯關係為與,即需要同時滿足多個權限標識才能執行相關方法,邏輯關係也可修改為或。

<code>@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermissions {
String[] value();
Logical logical() default Logical.AND;
}/<code>

在shiro配置中添加

<code>// 下面兩個方法對 註解權限起作用有很大的關係,請把這兩個方法,放在配置的最上面
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}/<code>

以及修改myshiroRealm

<code>public class MyShiroRealm extends AuthorizingRealm {

@Resource
private SysUserService sysUserService;
@Override

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 從session中獲取 user 對象
Session session = SecurityUtils.getSubject().getSession();
SysUser user = (SysUser)session.getAttribute("USER_SESSION");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 從shiro自動存儲的信息中獲取
//SysUser user =(SysUser) principals.getPrimaryPrincipal();
for(SysRole role:user.getRoleList()){
authorizationInfo.addRole(role.getRoleName());
for (SysPermission permissions : role.getSysPermissionList()){
authorizationInfo.addStringPermission(permissions.getPermissionName());
}
}

// 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;
}

/*主要是用來進行身份認證的,也就是說驗證用戶輸入的賬號和密碼是否正確。*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {

//獲取用戶的輸入的賬號.
String username = (String)token.getPrincipal();
//獲取用戶的輸入的密碼.
//String password = new String((char[])token.getCredentials());
//通過username從數據庫中查找 User對象,如果找到,沒找到.
//實際項目中,這裡可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
// sysUser.setPassword(password);

//ShiroEncryption shiroEncryption = new ShiroEncryption();
//進行加密
// String newpassword = shiroEncryption.shiroEncrypt(password,"libai");
SysUser userInfo = sysUserService.getUserInfoOrderName(username);
// SysUser userInfo = sysUserService.getUserInfo(username,newpassword);
if(userInfo == null){
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo, //用戶名
userInfo.getPassword(), //密碼
ByteSource.Util.bytes(userInfo.getSalt()),//salt=username+salt
getName() //realm name
);
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute("USER_SESSION", userInfo);
return authenticationInfo;
}

}
/<code>

最後進行測試

<code>/**
* RequiresAuthentication:
* 使用該註解標註的類,實例,方法在訪問或調用時,當前Subject必須在當前session中已經過認證。
* @return
*/
@RequiresAuthentication
@RequestMapping(value = "/1",method = RequestMethod.GET)
@ResponseBody
public String Hello0(){
return "session用戶 ";
}

/**
* RequiresUser
* 當前Subject必須是應用的用戶,才能訪問或調用被該註解標註的類,實例,方法。
* @return
*/
@RequiresUser

@RequestMapping(value = "/1",method = RequestMethod.GET)
@ResponseBody
public String Hello(){
return "應用用戶才能訪問 ";
}

/**
* RequiresRoles
* 當前Subject必須擁有所有指定的角色時,才能訪問被該註解標註的方法。如果當天Subject不同時擁有所有指定角色,
* 則方法不會執行還會拋出AuthorizationException異常。
* @return
*/
@RequiresRoles(value = "root")
@RequestMapping(value = "/2",method = RequestMethod.GET)
@ResponseBody
public String Hello1(){
return "特定角色才能訪問 ";
}

/**
* RequiresPermissions
* 當前Subject需要擁有某些特定的權限時,才能執行被該註解標註的方法。如果當前Subject不具有這樣的權限,則方法不會被執行。
* @return
*/
@RequiresPermissions(value = "delete")
@RequestMapping(value = "/3",method = RequestMethod.GET)
@ResponseBody
public String Hello2(){
return "特定權限才能訪問 ";
}

/**
* 組合權限 用於特定規定的
* @return
*/
@RequiresPermissions(value = {"delete","add"},logical = Logical.AND)

@RequestMapping(value = "/4",method = RequestMethod.GET)
@ResponseBody
public String Hello4(){
return "特定權限才能訪問 ";
}

/**
* 登錄效驗
* @param username
* @param password
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.GET)
@ResponseBody
public String login(String username, String password) {

Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
try {
subject.login(token);
return "登錄成功";
}catch (IncorrectCredentialsException e){
return "密碼錯誤";
}catch (LockedAccountException e){
return "登錄失敗,該用戶已被凍結";
}catch (AuthenticationException e){
return "該用戶不存在";
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/<code>

源碼下載地址清私信“shiro源碼”


分享到:


相關文章: