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是验证码部分


    org.springframework.boot
    spring-boot-starter-parent
    2.1.7.RELEASE
  

shiro-spring是用的最新的版本。patchca是用于验证码。


        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.apache.shiro
            shiro-spring
            1.4.1
        

        
            mysql
            mysql-connector-java
            runtime
        
        
        
             org.springframework.boot
             spring-boot-starter-data-jpa
        
        
        
            com.alibaba
            druid
            1.1.10
        

        
            log4j
            log4j
            1.2.17
        

        
            com.github.bingoohuang
            patchca
            0.0.1
        
    

配置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 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)

你可能感兴趣的:(Spring Boot整合Shiro)