spring boot 整合shiro(使用者授權和許可權控制)
(1) pom.xml中新增Shiro依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
(2) 注入Shiro Factory和SecurityManager
Shiro幾個核心的類,第一就是ShiroFilterFactory,第二就是SecurityManager,那麼最簡單的配置就是注入這兩個類就ok了
import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import java.util.LinkedHashMap; import java.util.Map; @Component @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>(); //anon:所有url都都可以匿名訪問; //authc: 需要認證才能進行訪問; //user:配置記住我或認證通過可以訪問; // 配置不會被攔截的連結 順序判斷 filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/authenRecharge/**", "anon"); // 配置不會被攔截的連結 順序判斷 filterChainDefinitionMap.put("/interface/**", "anon"); filterChainDefinitionMap.put("/manage/**", "anon"); filterChainDefinitionMap.put("/wicket/**", "anon"); filterChainDefinitionMap.put("/dbwizard/**", "anon"); filterChainDefinitionMap.put("/mallbook/login", "anon"); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/images/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/wicket/resource/**", "anon"); //配置退出 過濾器,其中的具體的退出程式碼Shiro已經替我們實現了 filterChainDefinitionMap.put("/logout", "logout"); //<!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最為下邊 -->:這是一個坑呢,一不小心程式碼就不好使了; //<!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問--> filterChainDefinitionMap.put("/**", "authc"); // 如果不設定預設會自動尋找Web工程根目錄下的"/login.jsp"頁面 shiroFilterFactoryBean.setLoginUrl("/"); // 登入成功後要跳轉的連結 shiroFilterFactoryBean.setSuccessUrl("/mallbook/MallHouse"); //未授權介面; shiroFilterFactoryBean.setUnauthorizedUrl("/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } //這個很重要,將我們自定義的Realm注入到SecurityManager中 @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; } /** * 憑證匹配器 *應為我們身份認證用的密碼是加密的,所以需要一個加密演算法 ,要是使用明文就不用了 * (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了 * 所以我們需要修改下doGetAuthenticationInfo中的程式碼; ) * * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("MD5");//雜湊演算法:這裡使用MD5演算法; hashedCredentialsMatcher.setHashIterations(1);//雜湊的次數,比如雜湊兩次,相當於 md5(md5("")); return hashedCredentialsMatcher; } //身份認證realm; @Bean public MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } /** * 開啟shiro aop註解支援. * 使用代理方式;所以需要開啟程式碼支援; * * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
(3) 身份認證,許可權控制
認證實現
Shiro的認證過程最終會交由Realm執行,這時會呼叫Realm的getAuthenticationInfo(token)方法。
該方法主要執行以下操作:
1、檢查提交的進行認證的令牌資訊
2、根據令牌資訊從資料來源(通常為資料庫)中獲取使用者資訊
3、對使用者資訊進行匹配驗證。
4、驗證通過將返回一個封裝了使用者資訊的AuthenticationInfo例項。
5、驗證失敗則丟擲AuthenticationException異常資訊。
而在我們的應用程式中要做的就是自定義一個Realm類,繼承AuthorizingRealm抽象類,過載doGetAuthenticationInfo (),重寫獲取使用者資訊的方法。
shiro的認證最終是交給了Realm進行執行了,所以我們需要自己重新實現一個Realm,此Realm繼承AuthorizingRealm。
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
public class MallBookShiroRealm extends AuthorizingRealm {
@Autowired
private UpmsUserService userService;
private final static Logger logger = LoggerFactory.getLogger(MallBookShiroRealm.class);
/**
* 許可權認證,為當前登入的Subject授予角色和許可權
* <p>
* 本例中該方法的呼叫時機為需授權資源被訪問時
* 並且每次訪問需授權資源時都會執行該方法中的邏輯,這表明本例中預設並未啟用AuthorizationCache
* 如果連續訪問同一個URL(比如重新整理),該方法不會被重複呼叫,Shiro有一個時間間隔(也就是cache時間,在ehcache-shiro.xml中配置),超過這個時間間隔再重新整理頁面,該方法會被執行
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
logger.info("許可權配置-->EpayShiroRealm.doGetAuthorizationInfo()");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
UserInfo userInfo = (UserInfo) principals.getPrimaryPrincipal();
for (RolesInfo role : userInfo.getRoles()) {
authorizationInfo.addRole(role.getName());
for (UpmsPermission p : role.getPermissions()) {
if (null != p.getPermissionValue() && p.getPermissionValue().length() > 0) {
authorizationInfo.addStringPermission(p.getPermissionValue());
}
}
}
return authorizationInfo;
}
/**
* 主要是用來進行身份認證的,也就是說驗證使用者輸入的賬號和密碼是否正確
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
logger.info("正在驗證身份...");
// 獲取使用者的輸入的賬號.
//將token轉換成UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = (String) token.getPrincipal();
System.out.println(token.getCredentials());
// 通過username從資料庫中查詢 User物件,如果找到,沒找到.
// 實際專案中,這裡可以根據實際情況做快取,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
UserInfo userInfo = userService.findByUsername(username);
System.out.println("----->>userInfo=" + userInfo);
if (null == userInfo) {
return null;
}
// 得到鹽值加密後的密碼:只用於方便資料庫測試,後期不會用到。
return new SimpleAuthenticationInfo(
userInfo,
userInfo.getPassword(),
ByteSource.Util.bytes(userInfo.getSalt()),
getName()
);
}
}
繼承AuthorizingRealm主要需要實現兩個方法:
doGetAuthenticationInfo();
doGetAuthorizationInfo();
其中doGetAuthenticationInfo主要是用來進行身份認證的,也就是說驗證使用者輸入的賬號和密碼是否正確。
SimpleAuthenticationInfo oauthenticationInfo =
return new SimpleAuthenticationInfo(
userInfo,//使用者名稱
userInfo.getPassword(),//密碼
ByteSource.Util.bytes(userInfo.getSalt()),//加密的鹽
getName()//realm name
);
doGetAuthorizationInfo()是許可權控制,當訪問到頁面的時候,使用了相應的註解或者shiro標籤才會執行此方法否則不會執行,所以如果只是簡單的身份認證沒有許可權的控制的話,那麼這個方法可以不進行實現,直接返回null即可。
主要的類:SimpleAuthorizationInfo
for (RolesInfo role : userInfo.getRoles()) {
authorizationInfo.addRole(role.getName());//新增角色
for (UpmsPermission p : role.getPermissions()) {
if (null != p.getPermissionValue() && p.getPermissionValue().length() > 0) {
authorizationInfo.addStringPermission(p.getPermissionValue());//新增選單
}
}
}
到這裡的話身份認證許可權控制基本是完成了,最後我們在編寫一個登入的時候,登入的處理
import java.util.ArrayList;
import java.util.List;
@RestController
public class LoginController{
Logger log= LoggerFactory.getLogger(LoginController.class);
@Autowired
private UpmsUserService userService;
@RequestMapping(value = "/ajaxLogin", method = RequestMethod.POST)
public Object login(String username, String password) {
String passwordNew= RSAUtils.decrypt(password, RSAUtils.getPrivateKey(Constant.PWD_PRIVATE_KEY));
UsernamePasswordToken token = new UsernamePasswordToken(username, passwordNew);
token.setHost("localhost");
Subject currentUser = SecurityUtils.getSubject();
UpmsResultConstant messageCode = UpmsResultConstant.LOGIN_SUCCESS;
if (null == userService.getUserInfo(token.getUsername())) {
return new UpmsResult(UpmsResultConstant.FAILED, UpmsResultConstant.FAILED.code);
}
try {
currentUser.login(token);
} catch (UnknownAccountException ex) {
messageCode = UpmsResultConstant.LOGIN_ACCOUNT_ERROR;
ex.printStackTrace();
} catch (IncorrectCredentialsException ex) {
messageCode = UpmsResultConstant.LOGIN_PASSWORD_ERROR;
ex.printStackTrace();
} catch (LockedAccountException ex) {
messageCode = UpmsResultConstant.LOGIN_ACCOUNT_LOCKED;
ex.printStackTrace();
} catch (ExcessiveAttemptsException ex) {
messageCode = UpmsResultConstant.LOGIN_EXCESSIVE_ERROR;
ex.printStackTrace();
} catch (AuthenticationException ex) {
messageCode = UpmsResultConstant.LOGIN_AUTHTICATION_FAIL;
ex.printStackTrace();
}
if (currentUser.isAuthenticated()) {
LoginInfo loginInfo = new LoginInfo();
UserInfo userInfo = (UserInfo) currentUser.getPrincipals().getPrimaryPrincipal();
loginInfo.setAvatar(userInfo.getAvatar());
loginInfo.setIntroduction(userInfo.getRole());
loginInfo.setName(userInfo.getUserName());
loginInfo.setToken(currentUser.getSession().getId().toString());
if (null != userInfo.getRoles() && userInfo.getRoles().size() > 0) {
List<String> roles = new ArrayList<>();
for (RolesInfo rolesInfo : userInfo.getRoles()) {
roles.add(rolesInfo.getName());
}
loginInfo.setRole(roles);
} else {
return new UpmsResult(UpmsResultConstant.FAILED, UpmsResultConstant.FAILED.code);
}
return new UpmsResult(UpmsResultConstant.SUCCESS, loginInfo);
} else {
return new UpmsResult(messageCode, 0);
}
}
@RequestMapping(value = "/getUserInfo")
public Object getUserInfo() {
UserInfo userInfo = UserUtils.getUser();
LoginInfo loginInfo = new LoginInfo();
loginInfo.setAvatar(userInfo.getAvatar());
loginInfo.setIntroduction(userInfo.getRole());
loginInfo.setName(userInfo.getRealName());
loginInfo.setToken(userInfo.getToken());
List<String> roles = new ArrayList<String>();
List<String> pms = new ArrayList<String>();
for (RolesInfo rolesInfo : userInfo.getRoles()) {
roles.add(rolesInfo.getTitle());
for (UpmsPermission permission : rolesInfo.getPermissions()) {
pms.add(permission.getPermissionValue());
}
}
loginInfo.setPms(pms);
loginInfo.setRole(roles);
loginInfo.setMchId(userInfo.getUserId().toString());
return new UpmsResult(UpmsResultConstant.SUCCESS, loginInfo);
}
}
這個時候訪問http://localhost:9527/#/login就可以跳轉到index頁面了
此時身份認證是好了,但是許可權控制好像還沒有作用
還需要兩部分:
第一就是開啟shiro aop註解支援
在ShiroConfig需要開啟shiro aop註解支援.
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
第二就是在controller方法中加入相應的註解:
@RequiresPermissions("sys_user_add")//許可權管理
@RestController
@RequestMapping(value = "/manage/user")
public class UserController {
@Autowired
private UpmsUserService userService;
private static Logger log = LoggerFactory.getLogger(UserController.class);
@Autowired
IdWorker idWorker;
@RequiresPermissions("user_manager")
@RequestMapping(value = "/list", method = RequestMethod.POST)
@ResponseBody
public Object list(UpmsUser user) {
return new UpmsResult(
UpmsResultConstant.SUCCESS
, userService.listUser(user)
);
}
@RequiresPermissions("sys_user_edit")
@RequestMapping(value = "/update", method = RequestMethod.POST)
public Object update(UpmsUser upmsUser) {
log.info("-------------------------------");
// 邏輯驗證
ComplexResult result = FluentValidator.checkAll()
.doValidate()
.result(ResultCollectors.toComplex());
if (!result.isSuccess()) {
return new UpmsResult(UpmsResultConstant.INVALID_LENGTH, result.getErrors());
}
String passwordNew = "";
if (StringUtils.isNotEmpty(upmsUser.getPassword())) {
passwordNew = RSAUtils.decrypt(upmsUser.getPassword(), RSAUtils.getPrivateKey(Constant.PWD_PRIVATE_KEY));
// 不允許直接改密碼
Object md = new SimpleHash("MD5", passwordNew, upmsUser.getSalt(), 1);
upmsUser.setPassword(md.toString());
upmsUser.setVerifyPassword(DictConstant.USER_HAS_LOGGED);
}
// upmsUser.setPassword(null);
int count = userService.updateUser(upmsUser);
return new UpmsResult(UpmsResultConstant.SUCCESS, count);
}
@RequiresPermissions("sys_user_edit")
@RequestMapping(value = "/updatePassword", method = RequestMethod.POST)
public Object updatePassword(UpmsUser upmsUser) {
String passwordNew = RSAUtils.decrypt(upmsUser.getPassword(), RSAUtils.getPrivateKey(Constant.PWD_PRIVATE_KEY));
// 不允許直接改密碼
Object md = new SimpleHash("MD5", passwordNew, ByteSource.Util.bytes(upmsUser.getSalt()), 1);
if (md.toString().equals(UserUtils.getUser().getPassword())) {
return new UpmsResult(UpmsResultConstant.PASSWORD_REPEAT, 0);
}
upmsUser.setPassword(md.toString());
upmsUser.setVerifyPassword(DictConstant.USER_HAS_LOGGED);
int count = userService.updateUser(upmsUser);
return new UpmsResult(UpmsResultConstant.SUCCESS, count);
}
@RequiresPermissions("sys_user_disabled")
@RequestMapping(value = "/locked", method = RequestMethod.POST)
public Object locked(UpmsUser upmsUser) {
int count = userService.updateUser(upmsUser.getUserId(), StatusEnum.FORBIDDEN.getByteVal());
return new UpmsResult(UpmsResultConstant.SUCCESS, count);
}
@RequiresPermissions("sys_user_enabled")
@RequestMapping(value = "/unlock", method = RequestMethod.POST)
public Object unlock(UpmsUser upmsUser) {
int count = userService.updateUser(upmsUser.getUserId(), StatusEnum.NORMAL.getByteVal());
return new UpmsResult(UpmsResultConstant.SUCCESS, count);
}
@RequestMapping(value = "listCurrentUser", method = RequestMethod.POST)
public Object listCurrentUser() {
return new UpmsResult(
UpmsResultConstant.SUCCESS
, userService.selectUserInfoById());
}
}
到此,身份認證及許可權管理已完成,如果還要加入快取機制,還要引入依賴