• 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)

  • 相关阅读:
    python 基础知识点整理 和详细应用
    DrawText的使用
    虚拟机无法联网解决方法
    Android中ExpandableListView控件基本使用
    PageRank算法
    怎样绕过工信部备案系统
    ASSERT函数
    一键安装 gitlab7 on rhel6.4 并设置邮件发送
    Android Bundle类
    ORACLE EXP命令
  • 原文地址:https://www.cnblogs.com/fzsyw/p/11373776.html
Copyright © 2020-2023  润新知