Spring Boot Shiro
本示例要內容
- 基於RBAC,授權、認證
- 加密、解密
- 統一異常處理
- redis session支援
介紹
Apache Shiro 是一個功能強大且易於使用的Java安全框架,可執行身份驗證,授權,加密和會話管理。藉助Shiro易於理解的API,您可以快速輕鬆地保護任何應用程式(從最小的移動應用程式到最大的Web和企業應用程式)。
開始使用
新增依賴
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
RBAC
RBAC 是基於角色的訪問控制(Role-Based Access Control )在 RBAC 中,許可權與角色相關聯,使用者通過成為適當角色的成員而得到這些角色的許可權。這樣管理都是層級相互依賴的,許可權賦予給角色,而把角色又賦予使用者,這樣的許可權設計很清楚,管理起來很方便。
建立實體類
- SysPermission.java
- SysRole.java
- UserInfo.java
採用 Jpa 技術會自動生成5張基礎表格,分別是:
- 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', '管理員', '7430bfdcc59212b32d78aacd42c7fe33', 'md5!@#', 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 來實現。使用 Filter是可以通過 URL 規則來進行過濾和許可權校驗,所以我們需要定義一系列關於 URL 的規則和訪問許可權。
@Configuration
@Slf4j
public class ShiroConfig {
@Autowired
private IgnoreAuthUrlProperties ignoreAuthUrlProperties;
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
log.info("Shiro過濾器開始處理");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 配置登入頁
shiroFilterFactoryBean.setLoginUrl("/login");
// 登入成功後跳轉頁面
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授權介面
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//攔截器
Map<String, String> filterMap = new LinkedHashMap<>();
//anon:所有url都都可以匿名訪問
Set<String> urlSet = new HashSet<>(ignoreAuthUrlProperties.getIgnoreAuthUrl());
urlSet.stream().forEach(temp -> filterMap.put(temp, "anon"));
//使用者未登入不進行跳轉,返回錯誤資訊
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("authc", new MyFormAuthenticationFilter());
//配置退出 過濾器
filterMap.put("/logout", "logout");
//authc:所有url都必須認證通過才可以訪問
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* 憑證匹配器
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//雜湊演算法:這裡使用MD5演算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//雜湊的次數,比如雜湊兩次,相當於 md5(md5(""));
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
@Bean
public AuthRealm authRealm() {
AuthRealm authRealm = new AuthRealm();
authRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return authRealm;
}
/**
* 安全管理器
*
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(authRealm());
return securityManager;
}
/**
* 啟用shiro註解
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 異常處理
*
* @return
*/
@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);
r.setDefaultErrorView("error");
r.setExceptionAttribute("ex");
return r;
}
}
Shiro 內建的兩個主要 Filter介紹
- anon:所有 url 都都可以匿名訪問
- authc: 需要認證才能進行訪問
認證和授權
@Slf4j
public class AuthRealm extends AuthorizingRealm {
@Resource
private UserInfoService userInfoService;
/**
* 授權
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("呼叫授權方法");
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;
}
/**
* 認證(主要是用來進行身份認證的,也就是說驗證使用者輸入的賬號和密碼是否正確)
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("呼叫認證方法");
//獲取使用者的輸入的賬號.
String username = (String) token.getPrincipal();
if (username == null) {
throw new AuthenticationException("賬號名為空,登入失敗!");
}
log.info("credentials:" + token.getCredentials());
UserInfo userInfo = userInfoService.findByUsername(username);
if (userInfo == null) {
throw new AuthenticationException("不存在的賬號,登入失敗!");
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo, //使用者
userInfo.getPassword(), //密碼
ByteSource.Util.bytes(userInfo.getCredentialsSalt()), //加鹽後的密碼
getName() //指定當前 Realm 的類名
);
return authenticationInfo;
}
}
登入
/**
* 登入
*
* @param username
* @param password
* @param map 如果出錯,回傳給前端的map
* @return
*/
@RequestMapping("/login")
public String login(String username, String password, Map<String, Object> map) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
String msg = "";
try {
subject.login(token);
} catch (UnknownAccountException e) {
msg = "賬號不存在!";
} catch (DisabledAccountException e) {
msg = "賬號未啟用!";
} catch (IncorrectCredentialsException e) {
msg = "密碼錯誤!";
} catch (Throwable e) {
msg = "未知錯誤!";
}
//判斷登入是否出現錯誤
if (msg.length() > 0) {
map.put("msg", msg);
return "/login";
} else {
return "redirect:index";
}
}
方法增加許可權驗證
/**
* 使用者新增
*
* @return
*/
@RequestMapping("/userAdd")
@RequiresPermissions("userInfo:add")
public String userInfoAdd() {
return "userInfoAdd";
}
這樣配置完,執行程式。只有使用者擁有userAdd許可權才允許訪問userAdd介面,否則會提示“未授權”訪問