SpringBoot中配置Shiro
習慣用eclipse開發
建立springboot專案前,先安裝spring tool外掛
開啟 help->Eclipse Marketplace
搜尋spring找到Spring Tools 3 Add-On外掛安裝,並重啟eclipse
這時候新建專案可以找到Spring Boot選項選擇New Spring Starter Project
填寫專案名等資訊
Next進入依賴選擇,一般都會用到Web依賴
點選Finish建立專案,可以在根路徑下找到xxxApplication.java的啟動類,執行main方法就可以啟動專案了
下面說明shiro的配置
首先引入依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.3</version>
</dependency>
使用@Configuration與@Bean註解,新增shiro基本配置
Configuration標註在類上,相當於把該類作為spring的xml配置檔案中的,作用為:配置spring容器(應用上下文)
securityManager是必須的。
在shiroFilter中:
loginUrl
successUrl:登入成功預設跳轉頁面,不配置則跳轉至”/”。如果登陸前點選的一個需要登入的頁面,則在登入自動跳轉到那個需要登入的頁面。不跳轉到此。
unauthorizedUrl:沒有許可權預設跳轉的頁面。
如果需要自定義一個過濾器
為了保持過濾器的執行順序,建立一個LinkedHashMap
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());
shiroFilter.setLoginUrl("/xxx");
shiroFilter.setUnauthorizedUrl("/user/unauthorized");
Map<String, String> filterChain = new LinkedHashMap<>();
filterChain.put("/user/logout", "logout");
filterChain.put("/user/login", "anon");
filterChain.put("/user/register", "anon");
filterChain.put("/user/unauthorized", "anon");
// filterChain.put("/**", "anon");
filterChain.put("/**", "token");
shiroFilter.setFilterChainDefinitionMap(filterChain);
Map<String, Filter> filters = new LinkedHashMap<>();
// filters.put("urlPerms", permFilter());
filters.put("token", tokenFilter());
shiroFilter.setFilters(filters);
return shiroFilter;
}
/**
* 此處不應將自定義Filter註冊為 @Bean 否則SpringBoot將載入此Filter導致ShiroFilter優先順序失效等一系列問題
* http://www.hillfly.com/2017/179.html
* @return
*/
public MyTokenFilter tokenFilter() {
return new MyTokenFilter();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
return securityManager;
}
@Bean
public MyShiroRealm shiroRealm() {
MyShiroRealm realm = new MyShiroRealm();
realm.setCredentialsMatcher(hashedCredentialsMatcher());
//沒有配置許可權快取,所以關閉授權快取域
realm.setAuthorizationCachingEnabled(false);
return realm;
}
/**
* 憑證匹配器
* (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了)
* HashedCredentialsMatcher說明:
* 使用者傳入的token先經過shiroRealm的doGetAuthenticationInfo方法
* 此時token中的密碼為明文。
* 再經由HashedCredentialsMatcher加密password與查詢使用者的結果password做對比。
* new SimpleHash("SHA-256", password, null, 1024).toHex();
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("SHA-256");//雜湊演算法:這裡使用SHA-256演算法;
hashedCredentialsMatcher.setHashIterations(1024);//雜湊的次數,比如雜湊兩次,相當於 MD5(MD5(""));
return hashedCredentialsMatcher;
}
}
另外我們還需要自定義ShiroRealm
realm,即 域。
SecurityManager通過realm來驗證使用者的身份和許可權
doGetAuthorizationInfo為授權方法-用於查詢使用者許可權、賦權
doGetAuthenticationInfo為認證方法-subject.login(token);執行時驗證使用者名稱密碼
public class MyShiroRealm extends AuthorizingRealm{
static final Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);
@Autowired
UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) arg0;
String username = token.getUsername();
logger.info("username = {}", username);
User user = userService.getUserByName(username);
logger.info("{}", null!=user?user.toJson():"null");
if(null != user) {
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName());
return info;
}
return null;
}
}
另外我還用到了一個過濾器,用作許可權驗證
isAccessAllowed方法中檢驗使用者是否有許可權獲取某一資源
AntPathMatcher實現了路徑萬用字元,例如user下所有資源 /user/**
這裡使用cookie和快取來校驗使用者
僅做參考
當權限被拒絕的時候,執行onAccessDenied
由於這個過濾器在shiro配置中沒有註冊
使用spring bean的時候需要用到通過ApplicationContext獲取bean
public class MyTokenFilter extends AccessControlFilter {
/**
* 專案啟動,該類在bean註冊前初始化,會報空指標, 所以, 需要使用的時候,在程式碼中用SpringUtil注入。
*/
private RoleUrlService roleUrlService;
private UserService userService;
private EhcacheUtil ehcacheUtil;
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
String permission = WebUtils.getPathWithinApplication(WebUtils.toHttp(request)).substring(0);
if (StringUtils.isEmpty(permission)) {
return true;
}
// 公共許可權驗證
roleUrlService = SpringUtil.getBean(RoleUrlService.class);
List<String> publicRole = roleUrlService.getPublicRole();
PatternMatcher matcher = new AntPathMatcher();
for (String uri : publicRole) {
if (null != uri && matcher.matches(uri, permission)) {
return true;
}
}
// Token驗證
HttpServletRequest rq = (HttpServletRequest) request;
Cookie token = null;
Cookie[] cookies = rq.getCookies();
if(null == cookies) {
return false;
}
for (Cookie cookie : cookies) {
if ("token".equals(cookie.getName())) {
token = cookie;
break;
}
}
if (null == token) {
return false;
}
ehcacheUtil = SpringUtil.getBean(EhcacheUtil.class);
UsernamePasswordToken upToken = (UsernamePasswordToken) ehcacheUtil
.get(EhCacheConstants.TOKEN_PREFIX + token.getValue());
if (null == upToken) {
return false;
}
userService = SpringUtil.getBean(UserService.class);
List<String> urlList = userService.findUserUrl(upToken.getUsername());
for (String uri : urlList) {
if (null != uri && matcher.matches(uri, permission)) {
return true;
}
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
if (!subject.isAuthenticated()) {
authenticationFailed(response);
return false;
}
return true;
}
/**
* 認證失敗
*
* @param response
* @throws IOException
*/
private void authenticationFailed(ServletResponse response) throws IOException {
response.setContentType("text/html;charset=UTF-8");
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.getWriter().write(JSON.toJSONString(Result.notLogin()));
}
}
最後我們需要一個登陸方法
@PostMapping("login")
public Result<String> login(HttpServletResponse response, UsernamePasswordToken token) {
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
UUID uuid = UUID.randomUUID();
// 把使用者登入資訊存入快取 key值為 TOKEN_{使用者標識}
ehcacheUtil.put(EhCacheConstants.TOKEN_PREFIX + uuid.toString(), token);
response.addCookie(new Cookie("token", uuid.toString()));
return new Result<>("登入成功");
} catch (IncorrectCredentialsException e) {
return new Result<>("密碼錯誤");
} catch (LockedAccountException e) {
return new Result<>("登入失敗,該使用者已被凍結");
} catch (AuthenticationException e) {
return new Result<>("該使用者不存在");
} catch (Exception e) {
e.printStackTrace();
}
return new Result<>("登入錯誤");
}