springboot 单体架构之shiro集成

2018-08-02 05:52:31来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

这里使用的是eclipse 开发工具

1.springboot 版本是2.0的,引入了2个shiro 的依赖,如下

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<!-- 安全框架:密码加密、权限控制、身份验证 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro+redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2-RELEASE</version>
</dependency>

 

2. shiro 主要分为2部分,第一部分是shiro的配置类,我放在src/main/resources添加config文件夹,新建ShiroConfig。

  首先,shiro会配置他的拦截器 shiroFilter(由于swagger和druid的页面也会被拦截,这里要放行,这是个坑),

  其次,会配置缓存管理方式,这里我使用的是 shiro-redis (是大神已经写好的redis的自动化管理,自己就不会去写更多的代码了,比如序列化的问题,redis存储问题等)

@Configuration
public class ShiroConfig {
@Value("${spring.redis.host}")
private String host;

@Value("${spring.redis.port}")
private int port;

// @Value("${spring.redis.timeout}") 
// private int timeout;


/**
* 修复Spring Boot整合shiro出现UnavailableSecurityManagerException 问题
* 此处设置相当于在web.xml中增加filter
*/
@Bean
public FilterRegistrationBean<DelegatingFilterProxy> delegatingFilterProxy() {
FilterRegistrationBean<DelegatingFilterProxy> filterRegistrationBean = new FilterRegistrationBean<DelegatingFilterProxy>();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}

/**
* SHIRO核心拦截器配置
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);

// 拦截器.顺序判断
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// authc:所有url都必须认证通过才可以访问
// anon:所有url都都可以匿名访问
filterChainDefinitionMap.put("/user/login", "anon");
// 放行静态资源
filterChainDefinitionMap.put("/static/**", "anon");
// swagger2 放行
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/swagger/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/docs/**", "anon");
// druid 监控放行
filterChainDefinitionMap.put("/druid/**", "anon");
// 退出 过滤器
filterChainDefinitionMap.put("/user/logout", "logout");
// 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边
// filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

// 配置退出过滤器,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index.html");
// 未授权要跳转的链接;
shiroFilterFactoryBean.setUnauthorizedUrl("/login.html");

return shiroFilterFactoryBean;
}

/**
* securityManager 安全管理器 (通过 authorize 调用 自定义 realm 数据进行认证和授权)
*/
@Bean(name = "securityManager")
public SecurityManager securityManager(@Qualifier("myRealm") MyRealm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(myRealm);

// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());

// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
return securityManager;
}

/**
* 自定义realm; (账号密码校验;权限等)
* 
* @return
*/
@Bean(name = "myRealm")
public MyRealm myRealm() {
MyRealm myRealm = new MyRealm();
// 添加 matcher加密算法到 MyRealm
myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myRealm;
}

/**
* 密码匹配凭证管理器
*
* @return
*/
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 采用MD5方式加密
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
// 设置加密次数
hashedCredentialsMatcher.setHashIterations(1024);
return hashedCredentialsMatcher;
}

/**
* Shiro生命周期处理器
* 
* @return
*/
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}

/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions 授权注解)
* 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
@Qualifier("securityManager") SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}

/**
* 配置shiro redisManager 使用的是shiro-redis开源插件
* 
* @return
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setExpire(1800);// 配置缓存过期时间
//redisManager.setTimeout(timeout);
// redisManager.setPassword(password);
return redisManager;
}

/**
* cacheManager 缓存 redis实现 使用的是shiro-redis开源插件
* 
* @return
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}

/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}

/**
* shiro session的管理
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}


/*@Bean(name = "redisTemplate")
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
template.setConnectionFactory(factory);
// key序列化方式
template.setKeySerializer(redisSerializer);
// value序列化
template.setValueSerializer(redisSerializer);
// value hashmap序列化
template.setHashValueSerializer(redisSerializer);
// key haspmap序列化
template.setHashKeySerializer(redisSerializer);
//
return template;
}*/
}

3.shiro 的另一个重要部分是realm的配置,其中包含2个方面,一是权限的自定义设定,二是登入认证的自定义设定,我放在src/main/resources添加realm文件夹,新建MyRealm。由于业务的原因,我这里只做了登入的认证,即加密算法MD5的认证

public class MyRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;

    /**
     * 在认证、授权内部实现机制中都有提到,最终处理都将交给Real进行处理。因为在Shiro中,最终是通过Realm来获取应用程序中的用户、角色及权限信息的。通常情况下,在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。可以说,Realm是专用于安全框架的DAO.
     * Shiro的认证过程最终会交由Realm执行,这时会调用Realm的getAuthenticationInfo(token)方法。
     * 该方法主要执行以下操作:
     *
     * 检查提交的进行认证的令牌信息 根据令牌信息从数据源(通常为数据库)中获取用户信息 对用户信息进行匹配验证。
     * 验证通过将返回一个封装了用户信息的AuthenticationInfo实例。
     * 验证失败则抛出AuthenticationException异常信息。而在我们的应用程序中要做的就是自定义一个Realm类,继承AuthorizingRealm抽象类,重载doGetAuthenticationInfo(),重写获取用户信息的方法。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    // 1.从主体传过来的认证信息中,获取用户名,密码
    String account = (String) token.getPrincipal();
    // String password = new String((char[]) token.getCredentials());

    // 2.通过用户名去到数据库中获取凭证
    AdminUser adminUser = this.userService.findUserByAccount(account);

    // 判断用户是否存在
    if (adminUser == null) {
        throw new UnknownAccountException();
    }
    // 判断用户是否有效
    if (adminUser.getAdminIsenable() == 0) {
        throw new LockedAccountException();
    }

    // 3.加密:MD5+salt,在这里添加盐值,以用户名作为盐值
    ByteSource salt = ByteSource.Util.bytes(adminUser.getAdminName());

    String realName = this.getName();

    // 4.验证凭证信息(密码信息)
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(account,
        adminUser.getAdminPassword(), salt, realName);

    // 5.当shiro验证成功,把用户信息放在session里
    Session session = SecurityUtils.getSubject().getSession(true); 
    session.setAttribute("activeUser", adminUser.getAdminAccount()); 

    return authenticationInfo;
    }

    /**
     * 授权用户权限 授权的方法是在碰到<shiro:hasPermission name=''></shiro:hasPermission>标签的时候调用的
     * ,它会去检测shiro框架中的权限(这里的permissions)是否包含有该标签的name值,如果有,里面的内容显示
     * ,如果没有,里面的内容不予显示(这就完成了对于权限的认证.)
     * 
     * shiro的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();
     * 当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行,所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回null即可。
     * 在这个方法中主要是使用类:SimpleAuthorizationInfo 进行角色的添加和权限的添加。
     * authorizationInfo.addRole(role.getRole());
     * authorizationInfo.addStringPermission(p.getPermission());
     * 当然也可以添加set集合:roles是从数据库查询的当前用户的角色,stringPermissions是从数据库查询的当前用户对应的权限
     * authorizationInfo.setRoles(roles);
     * authorizationInfo.setStringPermissions(stringPermissions);
     * 就是说如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “perms[权限添加]”);
     * 就说明访问/add这个链接必须要有“权限添加”这个权限才可以访问,
     * 如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”,
     * “roles[100002],perms[权限添加]”);
     * 就说明访问/add这个链接必须要有“权限添加”这个权限和具有“100002”这个角色才可以访问。
     * 
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    // 1.从主体传过来的认证信息中,获取用户名
    // String acoount = (String) principals.getPrimaryPrincipal();
    // 查询用户名称
    // User user = userService.findUserByAccount(acoount);
    // 添加角色和权限
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    // for (Role role:user.getRoles()) {
    // //添加角色
    // simpleAuthorizationInfo.addRole(role.getRoleName());
    // for (Permission permission:role.getPermissions()) {
    // //添加权限
    // simpleAuthorizationInfo.addStringPermission(permission.getPermission());
    // }
    // }
    return simpleAuthorizationInfo;

    }
}

4. 最后是在controller中,怎么调用这个认证,返回类型是我自己封装的一个类型,可以自己定义。这里需要注意的是,要去新建subject实例,才能去调用login验证。

    public Result<?> login(@RequestParam(value = "account") String account,
        @RequestParam("password") String password) {
    // 结果信息
    Result<?> result = new Result<>();
    // 创建Subject实例
    Subject subject = SecurityUtils.getSubject();

    // 将用户名及密码封装到UsernamePasswordToken
    UsernamePasswordToken token = new UsernamePasswordToken(account, password);

    try {
        // 完成登录验证
        subject.login(token);
        // 判断当前用户是否验证成功
        if (subject.isAuthenticated() == true) {
        // 登入成功
        ResultEnum resultEnum = ResultEnum.AdminSuccess;
        result = ResultUtil.getSuccess(resultEnum.getCode(), resultEnum.getMsg(),"");
        // 打印到控制台
        LogUtil.printLog(result.getMsg()+",登入用户: " + account);
        }

    } catch (LockedAccountException e) {
        // 用户无效
        throw new MyRuntimeException(ResultEnum.LockedAccountException);
    } catch (IncorrectCredentialsException e) {
        // 密码不正确
        throw new MyRuntimeException(ResultEnum.IncorrectCredentialsException);
    } catch (AuthenticationException e) {
        // 用户不存在
        throw new MyRuntimeException(ResultEnum.UnknownAccountException);
    } catch (Exception e) {
        throw e;
    }
    return result;
    }

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:适配器模式(Adapter Pattern)

下一篇:JAVA垃圾回收GARBAGE COLLECTION(二、垃圾收集器)