1. 程式人生 > >SpringBoot中配置Shiro

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<>("登入錯誤");
	}