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)

posted @ 2019-08-18 20:24  fzsyw  阅读(638)  评论(2编辑  收藏  举报