Spring Boot整合Shiro


概述

4A(認證Authentication、授權Authorization、賬號Account、審計Audit)是現代任何IT系統中很基礎但非常重要的部分,無論是傳統管理信息系統還是互聯網項目,出於保護業務數據和應用自身的安全,都會設計自己的登錄和資源授權策略。最近項目中需要登錄和權限相關的功能,項目為spring-boot工程,現在流行的權限驗證框架有shiro和spring-security,shiro相對spring-security來說學習難度要低一點,也是比較成熟的產品,因此選擇shiro作為項目的權限驗證框架。

步驟

添加依賴

spring boot的版本為2.1.7.RELEASE。如果大量依賴spring的項目,可以用https://start.spring.io/

patchca是驗證碼部分

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.7.RELEASE</version>
  </parent>

shiro-spring是用的最新的版本。patchca是用於驗證碼。

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <dependency>
        	 <groupId>org.springframework.boot</groupId>
        	 <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

		<dependency>
		    <groupId>com.github.bingoohuang</groupId>
		    <artifactId>patchca</artifactId>
		    <version>0.0.1</version>
		</dependency>
    </dependencies>

配置SecurityManager

在spring boot項目中去掉了復雜的各種xml配置,改為在Java文件中配置各種bean

@Bean(name = "securityManager")
public org.apache.shiro.mgt.SecurityManager defaultWebSecurityManager(@Autowired UserRealm 		userRealm) {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 關聯realm
    securityManager.setRealm(userRealm);
    securityManager.setRememberMeManager(rememberMeManager());
    return securityManager;
}

配置ShiroFilterFactoryBean

可以添加Filter,以及各種資源的權限類型anon、authc、user、perms、role。ShiroFilterFactoryBean(該類實現了FactoryBean接口,在IOC容器的基礎上給Bean的實現加上了一個簡單工廠模式和裝飾模式 我們可以在getObject()方法中靈活配置和擴展)

/**
 * 創建ShiroFilterFactoryBean shiro過濾bean
 */
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Autowired org.apache.shiro.mgt.SecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);

   /**
	* anon: 無需認證(登錄)可以訪問 
	* authc: 必須認證才可以訪問 
	* user: 如果使用rememberMe功能可以直接訪問
	* perms: 該資源必須得到資源權限才可以訪問 
	* role: 該資源必須得到角色權限才可以訪問
	*/
    Map<String, String> filerMap = new LinkedHashMap<>(); // 順序的map
    filerMap.put("/login", "anon");
    filerMap.put("/validCode", "anon");
    filerMap.put("/**", "authc");

    shiroFilterFactoryBean.setLoginUrl("/user/login.html");
    shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");
    shiroFilterFactoryBean.setSuccessUrl("/index");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filerMap);
    return shiroFilterFactoryBean;
}

創建和配置Realm

public class UserRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    
    /**
     * 執行授權邏輯
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) 	{
        System.out.println("執行授權邏輯1");
        //給資源進行授權,這里暫時寫死,實際需要從數據庫中獲取當前用戶的資源權限
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addStringPermission("user:add");
        info.addStringPermission("user:update");       
        return info;
    }

    /**
     * 執行認證邏輯
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {  
        ValidCodeUserPassWordToken token = (ValidCodeUserPassWordToken)authenticationToken;
        String validCode = token.getValidCode();
        if(StringUtils.isEmpty(validCode)) {
        	throw new AuthenticationException("未輸入驗證碼");
        }
        
        //校驗碼部分
        Subject subject = SecurityUtils.getSubject();
        ValidationCode oldValidCode = (ValidationCode);							   		   		    		subject.getSession().getAttribute("VALIDCODE");
        subject.getSession().removeAttribute("VALIDCODE");
        if(oldValidCode.isExpired()) {
        	throw new AuthenticationException("驗證碼已過期");
        }
        if(!oldValidCode.valid(validCode)) {
        	throw new AuthenticationException("驗證碼輸入錯誤");
        }
        
        //實際需要根據賬號,查詢當前用戶信息
        User user = new User();
        user.setId(123);
        user.setName("xs");
        user.setPassword("123");
        ByteSource salt= ByteSource.Util.bytes(user.getId().toString());
        Object password = new SimpleHash("MD5", user.getPassword(), salt, 2);
        return  new SimpleAuthenticationInfo(
                user, 
                "297254e9bfe0b8f39c682eda30bb9be0", //密碼
                salt,
                getName()
        );
    }   
}

配置UserRealm為Bean

	@Bean
    public UserRealm userRealm() {
	    UserRealm myShiroRealm = new UserRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());

        myShiroRealm.setCachingEnabled(true);
        //啟用身份驗證緩存,即緩存AuthenticationInfo信息,默認false
        myShiroRealm.setAuthenticationCachingEnabled(false);
        //緩存AuthenticationInfo信息的緩存名稱
        myShiroRealm.setAuthenticationCacheName("authenticationCache");
        //啟用授權緩存,即緩存AuthorizationInfo信息,默認false
        myShiroRealm.setAuthorizationCachingEnabled(true);
        //緩存AuthorizationInfo信息的緩存名稱
        myShiroRealm.setAuthorizationCacheName("authorizationCache");
        return myShiroRealm;
    }
	
	/**
     * 密碼加密
     */
    private HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");//散列算法:這里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(2);//散列的次數,比如散列兩次,相當於 
        return hashedCredentialsMatcher;
    }

權限注解

@RequestMapping("/add")
@RequiresPermissions("user:add")
public String add() {
	return "user/add";
}

校驗碼

/**
 * 校驗碼
 * @author Administrator
 *
 */
public class ValidationCode {
	
	public final static String VALID_CODE_NAME = "VALIDCODE";
	
	private String code;
	
	private Date createTime;
	
	private int expireMillisecond = 6000;
	
	private ValidationCode(String code) {
		this.code = code;
		this.createTime = new Date();
	}
	
	public static ValidationCode create(String code) {
		return new ValidationCode(code);
	}
	
    /**
     * 是否過期
     */
	public boolean isExpired() {
		Long between_Millisecond  =  new Date().getTime()-createTime.getTime();
		return between_Millisecond.intValue() > expireMillisecond;
	}
	
    /**
     * 與客戶端code比較是否一致
     */
	public boolean valid(String newCode) {
		return this.code.equalsIgnoreCase(newCode);
	}
}

檢驗碼生成類

@Controller
@RequestMapping
public class ValidationCodeController {
	@RequestMapping("/validCode")
	public void captcha(HttpServletRequest request, HttpServletResponse response,
			@RequestParam(name = "w", defaultValue = "90") Integer width,
			@RequestParam(name = "h", defaultValue = "38") Integer height,
			@RequestParam(name = "n", defaultValue = "4") Integer number) throws IOException {
		ConfigurableCaptchaService configurableCaptchaService = new ConfigurableCaptchaService();
		configurableCaptchaService.setColorFactory(new SingleColorFactory(new Color(25, 60, 170)));
		configurableCaptchaService
				.setFilterFactory(new CurvesRippleFilterFactory(configurableCaptchaService.getColorFactory()));
		RandomFontFactory randomFontFactory = new RandomFontFactory();
		randomFontFactory.setMinSize(30);
		randomFontFactory.setMaxSize(30);
        
		RandomWordFactory randomWordFactory = new RandomWordFactory();
		randomWordFactory.setMinLength(number);
		randomWordFactory.setMaxLength(number);
        
		configurableCaptchaService.setWordFactory(randomWordFactory);
		configurableCaptchaService.setFontFactory(randomFontFactory);
		configurableCaptchaService.setHeight(height);
		configurableCaptchaService.setWidth(width);
        
		response.setContentType("image/png");
		response.setHeader("Cache-Control", "no-cache, no-store");
		response.setHeader("Pragma", "no-cache");
		long time = System.currentTimeMillis();
		response.setDateHeader("Last-Modified", time);
		response.setDateHeader("Date", time);
		response.setDateHeader("Expires", time);

		// 將VALIDCODE放入Session中
		ServletOutputStream stream = null;
		try {
			HttpSession session = request.getSession();
			stream = response.getOutputStream();
			String validate_code = EncoderHelper.getChallangeAndWriteImage(configurableCaptchaService,
					"png", stream);
			session.setAttribute(ValidationCode.VALID_CODE_NAME, ValidationCode.create(validate_code));
			stream.flush();
		} finally {
			if (stream != null) {
				stream.close();
			}
		}
	}
}

統一異常處理

/**
 * 統一異常處理
 */
@RestController
@ControllerAdvice
public class ControllerExceptionHandler {
	private Logger logger = LoggerFactory.getLogger(getClass());

	@ExceptionHandler(DataAccessException.class)
	public Object handleDuplicateKeyException(DataAccessException e){
		logger.error(e.getMessage(), e);
		return ResultUtil.error("數據庫中已存在該記錄");
	}

	@ExceptionHandler(AuthorizationException.class)
	public Object handleAuthorizationException(AuthorizationException e){
		logger.error(e.getMessage(), e);
		return ResultUtil.error("沒有權限,請聯系管理員授權");
	}

	@ExceptionHandler(Exception.class)
	public Object handleException(Exception e){
		logger.error(e.getMessage(), e);
		return ResultUtil.error(e.getMessage());
	}
	
	@ExceptionHandler(IncorrectCredentialsException.class)
	public Object handleException(IncorrectCredentialsException e){
		logger.error(e.getMessage(), e);
		return ResultUtil.error("用戶名或者密碼不對");
	}
	
	@ExceptionHandler(UnknownAccountException.class)
	public Object handleException(UnknownAccountException e){
		logger.error(e.getMessage(), e);
		return ResultUtil.error("請輸入正確的賬戶");
	}
}

總結

目前搭建的項目,還沒有從數據庫獲取數據,登錄和權限獲取的數據目前都是寫死的。但是基本架子已經搭建好了,只需要在UserRealm中注入UserService類,提供數據庫獲取數據的服務即可。還有基於注解權限的方式需要注入LifecycleBeanPostProcessor和DefaultAdvisorAutoProxyCreator,並且DefaultAdvisorAutoProxyCreator.setProxyTargetClass(true)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM