springboot-shiro
springboot-shiro
簡介
shiro是apache下的一個輕量級開源專案,相對於springSecurity簡單的多。
三大功能模組:
- Subject:主體,一般指使用者;
- SecurityManager:安全管理器,管理所有Subject,可以配合內部安全元件;
- Realms:用於進行許可權資訊驗證,一般需要自己實現。
細分功能:
- Authentication:身份認證/登入;
- Authorization:授權;
- Session Manager:會話管理,即登入後的session;
- Cryptography:加密,密碼加密登;
- Web Support:web支援;
- Caching:快取,使用者資訊、角色、許可權等快取redis等快取中;
- Concurrency:多執行緒併發驗證;
- Testing:測試支援;
- Run As:允許一個使用者假裝另一個使用者(如果允許);
- Remember Me:記住密碼;
開始
新增依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${spring.shiro.version}</version>
</dependency>
密碼比較器
這個Bean定義了密碼的加密方式、加密鹽值和加密次數;當然也可以自己寫個類繼承HashedCredentialsMatcher
類,從而進一步自定義密碼匹配(例如:新增密碼錯誤次數限制);
@Bean
public CredentialsMatcher credentialsMatcher() {
// 如果要用redis,可以將RedisCacheManager作為構造引數
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//加密方式
credentialsMatcher.setHashAlgorithmName(md5);
//加密迭代次數
credentialsMatcher.setHashIterations(md5Time);
//true加密用的hex編碼,false用的base64編碼
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
認證和授權
這裡其實就是配置一個認證域,
- 認證:可以獲取使用者輸入的使用者名稱和密碼、token型別,可以設定密碼匹配規則,最後需要返回一個
AuthenticationInfo
交給shiro處理(這裡是可以進行很大程度上的自定義認證操作,例如:免密登入,豐富使用者資訊到session等); - 授權:主要是給使用者設定角色和許可權,需要注意的是,授權方法是在第一次訪問授權的地址時才會執行;
@Bean
public AuthorizingRealm pwdAuthorizingRealm(CredentialsMatcher credentialsMatcher) {
return new AuthorizingRealm(credentialsMatcher) {
// 認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 使用者輸入的使用者名稱和密碼
String userName = token.getPrincipal().toString();
// String userPwd = new String((char[]) token.getCredentials());
// todo: 根據使用者名稱從資料庫獲取密碼,這裡固定為:5678
String password = "25d55ad283aa400af464c76d713c07ad";
if (userName == null) {
return null;
}
// 自定義密碼加密鹽值,可以不要,預設沒有加密。也可以在這裡自定義密碼匹配。
ByteSource credentialsSalt = ByteSource.Util.bytes(md5Salt);
return new SimpleAuthenticationInfo(userName, password, credentialsSalt, getName());
}
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// todo: 可以根據登入名稱查詢角色、許可權資訊、同時還可以將角色資訊快取起來
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 設定角色
Set<String> roles = new HashSet<>();
roles.add(username);
roles.add("role_xxx");
info.setRoles(roles);
// 直接設定許可權
Set<String> stringSet = new HashSet<>();
stringSet.add(username);
info.setStringPermissions(stringSet);
return info;
}
};
}
redis
開始
使用redis管理快取和會話(可以不要);
-
參考springboot在專案中引入redis;
-
新增依賴
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
Bean
配置好這些Bean並不代表就成功整合redis了,還要在後面的會話管理器、許可權管理器中注入這些Bean;
@Bean
public RedisManager redisManager(RedisProperties redisProperties) {
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisProperties.getHost() + ":" + redisProperties.getPort());
redisManager.setPassword(redisProperties.getPassword());
redisManager.setTimeout(1800);
return redisManager;
}
@Bean
public RedisSessionDAO redisSessionDAO(RedisManager redisManager) {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager);
return redisSessionDAO;
}
@Bean
public RedisCacheManager redisCacheManager(RedisManager redisManager) {
RedisCacheManager cacheManager = new RedisCacheManager();
cacheManager.setRedisManager(redisManager);
return cacheManager;
}
會話管理器
可以設定一些會話管理器的引數,例如是否使用redis;
@Bean
public DefaultWebSessionManager defaultWebSessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 這兩行用於整合redis
// sessionManager.setSessionDAO(sessionDAO);
// sessionManager.setCacheManager(cacheManager);
return sessionManager;
}
許可權管理器
可以設定一些會話管理器的引數,例如是否使用redis快取,配置一個或者多個Realm域
@Bean
public DefaultWebSecurityManager securityManager(AuthorizingRealm pwdAuthorizingRealm, DefaultWebSessionManager sessionManager) {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
// 這行用於整合redis
// defaultSecurityManager.setCacheManager(cacheManager);
defaultSecurityManager.setSessionManager(sessionManager);
defaultSecurityManager.setRealm(pwdAuthorizingRealm);
return defaultSecurityManager;
}
自定義filterMap
這其實就是個Map,可以在這個map中自定義shiro許可權過濾器,其中Map的key為要自定義的許可權名(如:roles、perms、authc等),Map的value為自定義的過濾方法,下面的示例程式碼實現了將角色過濾的判斷邏輯有原來預設的and改為or(如果不需要自定義,可以不要這個Map);
public Map<String, Filter> filterMap(){
Map<String, Filter> filterMap = new HashMap<>();
// 自定義角色過濾器,將and改成or,同時role無許可權返回json
filterMap.put("roles", new RolesAuthorizationFilter() {
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String[] roles = (String[]) mappedValue;
if (roles == null || roles.length == 0) {
return true;
}
Subject subject = getSubject(request, response);
for (String role : roles) {
if (subject.hasRole(role)) return true;
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
Subject subject = getSubject(request, response);
HttpServletResponse res = (HttpServletResponse) response;
res.setContentType("application/json;charset=utf-8");
if (subject.getPrincipal() == null) {
saveRequestAndRedirectToLogin(request, response);
/*res.setStatus(HttpStatus.UNAUTHORIZED.value());
res.getWriter().write("請先登入");*/
} else {
res.setStatus(HttpStatus.FORBIDDEN.value());
res.getWriter().write("您沒有訪問許可權");
}
return false;
}
});
// 自定義登入攔截,未登入的訪問直接返回json,而不是跳轉
/*filterMap.put("authc", new UserFilter(){
@Override
protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
HttpServletResponse res = (HttpServletResponse) response;
res.setStatus(HttpStatus.UNAUTHORIZED.value());
res.setContentType("application/json;charset=utf-8");
res.getWriter().write("請先登入");
}
});*/
return filterMap;
}
資源許可權
這個其實就是LinkedHashMap,需要注意的是這是個有順序的Map,先插入的許可權資料優先判斷,另外這個Map的鍵是訪問url規則,值為具體許可權,取值有:
- authc:所有url都必須認證通過才可以訪問;
- anon:所有url都都可以匿名訪問
- user:如果使用rememberMe的功能可以直接訪問
- perms: 該資源必須得到資源許可權可以訪問
- roles: 該資源必須得到角色許可權才能訪問
public Map<String, String> findFilterChainDefinitionMap(){
// todo: 後面這個可以直接從資料庫裡面獲取
// 注意這個map
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//按順序依次判斷
filterChainDefinitionMap.put("/favicon.ico", "anon");
filterChainDefinitionMap.put("/login/login", "anon");
filterChainDefinitionMap.put("/login/login/pwd", "anon");
filterChainDefinitionMap.put("/login/logout", "logout");
filterChainDefinitionMap.put("/admin", "roles[admin]");
filterChainDefinitionMap.put("/user", "roles[user]");
// 注意許可權設定順序
filterChainDefinitionMap.put("/a/1", "roles[admin]");
filterChainDefinitionMap.put("/a/**", "roles[user, admin]");
// 這種預設是and,即同時擁有admin和user才能訪問
filterChainDefinitionMap.put("/info", "roles[admin,user]");
filterChainDefinitionMap.put("/perms", "perms[admin]");
filterChainDefinitionMap.put("/**", "authc");
return filterChainDefinitionMap;
}
ShiroFilterFactoryBean
Shiro的核心配置類
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 上面自定義的filterMap
shiroFilterFactoryBean.setFilters(shiroService.filterMap());
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/login/login");
shiroFilterFactoryBean.setUnauthorizedUrl("/deny");
// 上面編寫資源許可權Map
Map<String, String> filterChainDefinitionMap = shiroService.findFilterChainDefinitionMap();
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
登入
@PostMapping("/login")
public String login(String username, String password, Model model, HttpServletRequest req) {
// 在認證提交前準備 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 執行認證登陸
// 可以獲取登入前的訪問request物件
String url = WebUtils.getSavedRequest(req).getRequestURI();
System.out.println(url);
// 從SecurityUtils裡邊建立一個 subject
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
} catch (AuthenticationException e) {
// 包括未知賬戶、密碼錯誤、使用者名稱或密碼錯誤次數過多、賬戶已鎖定、使用者名稱或密碼不正確等異常
model.addAttribute("msg", e.getMessage());
}
if (subject.isAuthenticated()) {
Session session = subject.getSession();
// session.setTimeout(30 * 1000);
session.setAttribute("user", "這裡有登入資訊");
model.addAttribute("msg", "登入成功");
return "index";
} else {
token.clear();
}
return "login";
}