最近在由Spring Boot2.x構建的更簡潔的後臺管理系統,完美整合SpringMvc + Shiro + MybatisPlus + Beetl技術,項目開發完成會開源出來,希望能對大家學習道路上有所幫助。在這一篇中我將把我整合Shiro過程記錄下來,希望對大家的學習這塊能有所幫助。
maven依賴包
<!-- shiro框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!--shiro依賴和緩存-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
<version>1.4.0</version>
</dependency>
Shiro 配置類
/**
* Shiro配置中心
*
* @Auther: hrabbit
* @Date: 2018-12-24 12:33 PM
* @Description:
*/
@Configuration
public class ShiroConfig {
/**
* Shiro的過濾器鏈
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
/**
* 默認登錄路徑
*/
shiroFilter.setLoginUrl("/login");
/**
* 登錄成功後要跳轉的鏈接
*/
shiroFilter.setSuccessUrl("/");
/**
* 沒有權限的時候跳轉頁面
*/
shiroFilter.setUnauthorizedUrl("/global/error");
/**
* 配置shiro攔截器鏈
*
* anon 不需要認證
* authc 需要認證
* user 驗證通過或RememberMe登錄的都可以
*
* 當應用開啓了rememberMe時,用戶下次訪問時可以是一個user,但不會是authc,因爲authc是需要重新認證的
*
* 順序從上到下,優先級依次降低
*
* api開頭的接口,走rest api鑑權,不走shiro鑑權
*
*/
Map<String, String> hashMap = new LinkedHashMap<>();
hashMap.put("/static/**", "anon");
hashMap.put("/login", "anon");
hashMap.put("/global/sessionError", "anon");
hashMap.put("/**", "user");
shiroFilter.setFilterChainDefinitionMap(hashMap);
return shiroFilter;
}
/**
* 憑證匹配器
* (由於我們的密碼校驗交給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 ShiroRealm shiroDbRealm() {
ShiroRealm shiroDbRealm = new ShiroRealm();
shiroDbRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroDbRealm;
}
}
注意:裏面的 SecurityManager
類導入的應該是 import org.apache.shiro.mgt.SecurityManager
;
shirFilter 方法中主要是設置了一些重要的跳轉 url,比如未登陸時setLoginUrl
,無權限時的跳轉setUnauthorizedUrl
權限攔截 Filter
當運行一個Web應用程序時,Shiro
將會創建一些有用的默認 Filter
實例,並自動地將它們置爲可用,而這些默認的 Filter
實例是被 DefaultFilter
枚舉類定義的,當然我們也可以自定義 Filter
實例
Filter | 解釋 |
---|---|
anon | 無參,開放權限,可以理解爲匿名用戶或遊客 |
authc | 無參,需要認證 |
logout | 無參,註銷,執行後會直接跳轉到shiroFilterFactoryBean.setLoginUrl(); 設置的 url |
authcBasic | 無參,表示 httpBasic 認證 |
user | 無參,表示必須存在用戶,當登入操作時不做檢查 |
ssl | 無參,表示安全的URL請求,協議爲 https |
perms[user] | 參數可寫多個,表示需要某個或某些權限才能通過,多個參數時寫 perms["user, admin"],當有多個參數時必須每個參數都通過纔算通過 |
roles[admin] | 參數可寫多個,表示是某個或某些角色才能通過,多個參數時寫 roles["admin,user"],當有多個參數時必須每個參數都通過纔算通過 |
rest[user] | 根據請求的方法,相當於 perms[user:method],其中 method 爲 post,get,delete 等 |
port[8081] | 當請求的URL端口不是8081時,跳轉到schemal://serverName:8081?queryString 其中 schmal 是協議 http 或 https 等等,serverName 是你訪問的 Host,8081 是 Port 端口,queryString 是你訪問的 URL 裏的 ? 後面的參數 |
常用的主要就是 anon,authc,user,roles,perms 等
注意:anon, authc, authcBasic, user 是第一組認證過濾器,perms, port, rest, roles, ssl 是第二組授權過濾器,要通過授權過濾器,就先要完成登陸認證操作(即先要完成認證才能前去尋找授權) 才能走第二組授權器(例如訪問需要 roles 權限的 url,如果還沒有登陸的話,會直接跳轉到 shiroFilterFactoryBean.setLoginUrl(); 設置的 url
自定義 realm 類
我們首先要繼承 AuthorizingRealm
類來自定義我們自己的 realm
以進行我們自定義的身份,權限認證操作。
/**
* 自定義Shiro規則
* @Auther: hrabbit
* @Date: 2018-11-21 1:16 PM
* @Description:
*/
@Slf4j
public class MyShiroRealm extends AuthorizingRealm {
@Resource
private SysModuleOperationService sysModuleOperationService;
@Resource
private SysUsersService sysUsersService;
@Resource
private SysRolesService sysRolesService;
/**
* 資源認證
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
ShiroUser userInfo = (ShiroUser) principals.getPrimaryPrincipal();
//按鈕資源
Set<String> permissionSet = new HashSet<>();
//用戶角色
Set<String> roleNameSet = new HashSet<>();
//獲取用戶的角色集合
List<Integer> roleList = userInfo.getRoleList();
for (Integer roleId:roleList){
//根據角色id獲取到資源信息
List<ModuleOperation> allMenuByUserId = sysModuleOperationService.getPermissionByRoleId(roleId);
for (ModuleOperation moduleOperation:allMenuByUserId){
if (ToolUtil.isNotEmpty(moduleOperation.getCode()))
permissionSet.add(moduleOperation.getCode());
}
//查詢角色信息
Roles roles = sysRolesService.selectById(roleId);
if (roles!=null && ToolUtil.isNotEmpty(roles.getRoleCode())){
roleNameSet.add(roles.getRoleCode());
}
}
//添加按鈕資源
authorizationInfo.addStringPermissions(permissionSet);
//添加角色
authorizationInfo.addRoles(roleNameSet);
return authorizationInfo;
}
/**
* 主要是用來進行身份認證的,也就是說驗證用戶輸入的賬號和密碼是否正確。
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
//獲取shiroFactory工廠
ShiroFactoryService shiroFactory = ShiroFactroy.me();
//獲取到用戶的信息
UsernamePasswordToken userToken = (UsernamePasswordToken)token;
//獲取用戶的輸入的賬號.
String username = userToken.getUsername();
//實際項目中,這裏可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
ShiroUser userInfo = sysUsersService.getShiroUserByLoginName(username);
SysUsers sysUser = sysUsersService.getSysUsersByLoginName(username);
//創建緩存用戶信息
SimpleAuthenticationInfo info = shiroFactory.info(userInfo,sysUser,super.getName());
return info;
}
/**
* 設置認證加密方式
*/
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher md5CredentialsMatcher = new HashedCredentialsMatcher();
md5CredentialsMatcher.setHashAlgorithmName(ShiroUtils.hashAlgorithmName);
md5CredentialsMatcher.setHashIterations(ShiroUtils.hashIterations);
super.setCredentialsMatcher(md5CredentialsMatcher);
}
}
重寫的兩個方法分別是實現身份認證以及權限認證,shiro
中有個作登陸操作的 Subject.login()
方法,當我們把封裝了用戶名,密碼的 token
作爲參數傳入,便會跑進這兩個方法裏面(不一定兩個方法都會進入)
其中 doGetAuthorizationInfo
方法只有在需要權限認證時纔會進去,比如前面配置類中配置了 filterChainDefinitionMap.put("/**", "user");
的管理員角色,這時進入系統時就會進入 doGetAuthorizationInfo
方法來檢查權限;而 doGetAuthenticationInfo
方法則是需要身份認證時(比如前面的 Subject.login()
方法)纔會進入
再說下 UsernamePasswordToken
類,我們可以從該對象拿到登陸時的用戶名和密碼(登陸時會使用 new UsernamePasswordToken(username, password);)
,而 get 用戶名或密碼有以下幾個方法
//獲得用戶名 String
token.getUsername();
//獲得用戶名 Object
token.getPrincipal();
//獲得密碼 char[]
token.getPassword();
//獲得密碼 Object
token.getCredentials();
LoginController的實現
/**
* 登錄控制器
* @Auther: hrabbit
* @Date: 2018-11-19 10:23 AM
* @Description:
*/
@Controller
@Slf4j
@Api(value = "登錄API",description = "登錄、登出驗證,跳轉主界面")
public class LoginController extends BaseController {
/**
* 基礎路徑
*/
private static String BASEURL = "modual";
@Autowired
private SysModuleOperationService sysModuleOperationService;
@Autowired
private SysUsersService sysUsersService;
/**
* 跳轉到主頁
* @return
*/
@RequestMapping(value = {"/","/index"},method = RequestMethod.GET)
@ApiOperation(value="跳轉到主界面", notes="跳轉到主頁面,查詢用戶角色信息和頁面信息")
public String index(ModelMap model){
//獲取用戶角色idf
List<Integer> roleList = ShiroUtils.getUser().getRoleList();
//如果用戶不存在角色,跳轉到登錄界面
if (roleList == null || roleList.size() == 0){
ShiroUtils.getSubject().logout();
model.addAttribute("msg","該用戶沒有角色,無法登陸");
return "login";
}
//根據角色id查詢按鈕資源
List<MenuNode> menuNodes = sysModuleOperationService.getAllMenuByRoleId(roleList);
menuNodes = MenuNode.buildTitle(menuNodes);
//返回用戶資料信息
ShiroUser shiroUser = ShiroUtils.getUser();
//將Shiro用戶信息返回到前端頁面
model.addAttribute("user",shiroUser);
model.addAttribute("title",menuNodes);
return BASEURL+"/index.html";
}
/**
* 跳轉到登錄界面
* @return
*/
@RequestMapping(value = "login",method = RequestMethod.GET)
@ApiOperation(value="跳轉到登錄界面", notes="跳轉到登錄界面")
public String login(){
if (ShiroUtils.isAuthenticated() || ShiroUtils.getUser()!=null){
return REDIRECT+ "/";
}else{
return "login.html";
}
}
/**
* 頁面提交登錄
*
* @param username 登錄名稱
* @param password 用戶密碼
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.POST)
@ApiOperation(value="表單驗證", notes="提交登錄信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "username",value = "用戶名稱",required = true,dataType = "String"),
@ApiImplicitParam(name = "password",value = "用戶密碼",required = true,dataType = "String")
})
public String login(String username,String password){
Subject subject = ShiroUtils.getSubject();
//檢驗用戶是否存在
SysUser sysUser = sysUserService.findByLoginName(username);
// 在認證提交前準備 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 執行認證登陸
subject.login(token);
ShiroUser shiroUser = ShiroUtils.getUser();
//將ShiroUser對象存儲到session中
HttpUtils.getRequest().getSession().setAttribute("shiroUser",shiroUser);
//保存Session狀態
ShiroUtils.getSession().setAttribute("sessionFlag",true);
return REDIRECT+"/";
}
/**
* 退出登錄
* @return
*/
@RequestMapping(value = "loginOut",method = RequestMethod.GET)
@ApiOperation(value="退出登錄", notes="返回登錄界面")
public String loginOut(){
ShiroUtils.getSubject().logout();
return REDIRECT+"login.html";
}
}
這裏我們需要注意創建異常攔截器,這樣當用戶名或者密碼不正確的時候,Shiro會自動拋出異常,我們只需要將異常捕獲即可
/**
* 異常類
*
* @Auther: hrabbit
* @Date: 2018-11-15 3:40 PM
* @Description:
*/
@ControllerAdvice("com.hrabbit.admin")
@Order(-1)
@Slf4j
public class GlobalExceptionHandler {
/**
* 其他異常拋出信息
*
* @param response
* @param ex
* @return
*/
@ExceptionHandler(Exception.class)
public BaseResponse otherExceptionHandler(HttpServletResponse response, Exception ex) {
response.setStatus(500);
log.error(ex.getMessage(), ex);
return new BaseResponse(500, ex.getMessage());
}
/**
* 賬號被凍結異常
*/
@ExceptionHandler(DisabledAccountException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public String accountLocked(DisabledAccountException e, Model model) {
model.addAttribute("message", "賬號被凍結");
return "/login.html";
}
/**
* 賬號密碼錯誤異常
*/
@ExceptionHandler(CredentialsException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public String credentials(CredentialsException e, Model model) {
model.addAttribute("message", "賬號密碼錯誤");
return "/login.html";
}
}
測試
密碼錯誤的時候,會自動捕獲到異常信息
密碼正確,進入到主頁面
代碼正在編寫中,等這塊我編寫完成,會放到碼雲上面的
碼雲地址: https://gitee.com/hrabbit/hrabbit-admin
個人博客:http://www.hrabbit.xin