Springboot之安全管理整合Shiro

发布时间:2023-09-06 09:30

简单整合

使用springboot整合shiro 实现登录、权限控制。
1、用户类、角色类、权限类

@Data
@AllArgsConstructor
public class User {

    private String token;
    private String username;
    private String password;
    /**
     * 用户对应的角色集合
     */
    private Set<Role> roles;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public User() {
    }
}

@Data
@AllArgsConstructor
public class Role {

    private String id;
    private String roleName;
    /**
     * 角色对应权限集合
     */
    private Set<Permissions> permissions;
}

@Data
@AllArgsConstructor
public class Permissions {
    private String id;
    private String permissionsName;
}

2、maven文件

 <dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
    <groupId>org.projectlombokgroupId>
    <artifactId>lombokartifactId>
    <optional>trueoptional>
dependency>


<dependency>
    <groupId>org.apache.shirogroupId>
    <artifactId>shiro-springartifactId>
    <version>${shiro-spring.version}version>
dependency>

3、realm文件,配置登录验证、权限逻辑

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

    /**
     * 授权逻辑
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行授权逻辑");
        //获取登录用户名
        Object primaryPrincipal = principalCollection.getPrimaryPrincipal();
        //查询用户名称
        User user = (User) primaryPrincipal;
        //添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (Role role : user.getRoles()) {
            //添加角色
            simpleAuthorizationInfo.addRole(role.getRoleName());
            //添加权限
            for (Permissions permissions : role.getPermissions()) {
                simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
            }
        }
        return simpleAuthorizationInfo;
    }

    /**
     * 认证逻辑
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行认证逻辑");
        // 编写shiro判断逻辑,判断用户名和密码
        System.out.println(authenticationToken.toString());
        // 1. 判断用户名
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        User byUsername = userService.getByUsername(username);
        if (byUsername == null) {
            //用户名不存在
            return null;//shiro底层会抛出UnknownAccountException
        }
        // 2. 判断密码
        // 参数1:需要返回给login方法的数据;参数2:数据库密码,shiro会自动判断
        return new SimpleAuthenticationInfo(byUsername, byUsername.getPassword(), "");
    }
}

4、编写shiro配置类

@Configuration
public class ShiroConfig {

    //Filter工厂,设置对应的过滤条件和跳转条件
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        //登出
        map.put("/logout", "logout");
        //对所有用户认证
        map.put("/**", "authc");
        //登录
        shiroFilterFactoryBean.setLoginUrl("/login");
//        //首页
//        shiroFilterFactoryBean.setSuccessUrl("/index");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }


    /**
     * 权限管理,配置主要是Realm的管理认证
     * 创建DefaultWebSecurityManager
     * @return
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        return securityManager;
    }

    /**
     * 创建Realm
     */

    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }

    /**
     * 开启Shiro注解(如@RequiresRoles,@RequiresPermissions),
     * 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 开启aop注解支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

}

5、编写userService模拟数据库查询

@Service
public class UserService {
    //模拟从数据库中查询数据
    private volatile static Map<String, User> users = new ConcurrentHashMap<>();
    static {
        Permissions permissions1 = new Permissions("1", "select");
        Permissions permissions2 = new Permissions("2", "add");
        //拥有所有权限
        Set<Permissions> permissionsSet = new HashSet<>();
        permissionsSet.add(permissions1);
        permissionsSet.add(permissions2);
        Role role = new Role("1", "admin", permissionsSet);
        Set<Role> roleSet = new HashSet<>();
        roleSet.add(role);
        User user = new User("1", "kaico", "123456", roleSet);
        users.put(user.getUsername(), user);

        //至于查询的权限
        Set<Permissions> permissionsSet1 = new HashSet<>();
        permissionsSet1.add(permissions1);
        Role role1 = new Role("2", "user", permissionsSet1);
        Set<Role> roleSet1 = new HashSet<>();
        roleSet1.add(role1);
        User user1 = new User("2", "jing", "123456", roleSet1);
        users.put(user1.getUsername(), user1);
    }

    public User getByUsername(String username){

        return users.get(username);
    }
}

6、编写全局异常捕获类,用于处理没有权限抛的异常

@ControllerAdvice
@Slf4j
public class MyExceptionHandler {

    @ExceptionHandler
    @ResponseBody
    public String ErrorHandler(AuthorizationException e) {
        log.error("没有通过权限验证!", e);
        return "没有通过权限验证!";
    }
}

7、编写测试类

@RestController
@Log4j2
public class TestController {

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login(String username, String password){
        System.out.println("login");
        //使用shiro编写认证操作
        //获取Subject
        Subject subject = SecurityUtils.getSubject();
        //封装用户数据
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            //进行验证,这里可以捕获异常,然后返回对应信息
            subject.login(token);
        } catch (UnknownAccountException e) {
            log.error("用户名不存在!", e);
            return "用户名不存在!";
        } catch (AuthenticationException e) {
            log.error("账号或密码错误!", e);
            return "账号或密码错误!";
        } catch (AuthorizationException e) {
            log.error("没有权限!", e);
            return "没有权限";
        }
        return "login success";
    }

    @RequestMapping(value = "/add", method = RequestMethod.GET)
    @RequiresPermissions(value = "add")
    public String add(){
        System.out.println("add");

        return "add";
    }

    @RequestMapping(value = "/select", method = RequestMethod.GET)
    @RequiresPermissions(value = "select")
    public String select(){
        System.out.println("select");
        return "select";
    }


    @RequestMapping(value = "/loginOut", method = RequestMethod.GET)
    public String loginOut(){
        //获取Subject
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return "loginOut";
    }

    @RequestMapping(value = "/error", method = RequestMethod.GET)
    public String error(){
        return "error";
    }

}

shiro密码加密管理

数据库中保存的密码都是明文的,一旦数据库数据泄露,那就会造成不可估算的损失,所以我们通常都会使用非对称加密,简单理解也就是不可逆的加密,而 md5 加密算法就是符合这样的一种算法。

既然相同的密码 md5 一样,那么我们就让我们的原始密码再加一个随机数,然后再进行 md5 加密,这个随机数就是我们说的盐(salt),这样处理下来就能得到不同的 Md5 值,当然我们需要把这个随机数盐也保存进数据库中,以便我们进行验证。

1、修改realm的验证配置:主要修改返回的 SimpleAuthenticationInfo 对象增加盐参数。

/**
     * 认证逻辑
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行认证逻辑");
        // 编写shiro判断逻辑,判断用户名和密码
        System.out.println(authenticationToken.toString());
        // 1. 判断用户名
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        User byUsername = userService.getByUsername(username);
        if (byUsername == null) {
            //用户名不存在
            return null;//shiro底层会抛出UnknownAccountException
        }
        // 2. 判断密码
        // 参数1:需要返回给login方法的数据;参数2:数据库密码,shiro会自动判断 参数3:密码加密的盐
        return new SimpleAuthenticationInfo(byUsername, byUsername.getPassword(),  ByteSource.Util.bytes(username), "");
    }

2、修改shiro配置文件:给userRealm bean 设置加密算法

 @Bean
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();

        // 设置加密算法
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher("md5");
        // 设置加密次数
        credentialsMatcher.setHashIterations(1);
        userRealm.setCredentialsMatcher(credentialsMatcher);
        return userRealm;
    }

3、模拟的数据库数据
Springboot之安全管理整合Shiro_第1张图片

session管理

shiro中的session特性

  1. 基于POJO/J2SE:shiro中session相关的类都是基于接口实现的简单的java对象(POJO),兼容所有java对象的配置方式,扩展也更方便,完全可以定制自己的会话管理功能 。
  2. 简单灵活的会话存储/持久化:因为shiro中的session对象是基于简单的java对象的,所以你可以将session存储在任何地方,例如,文件,各种数据库,内存中等。
  3. 容器无关的集群功能:shiro中的session可以很容易的集成第三方的缓存产品完成集群的功能。例如,Ehcache + Terracotta, Coherence, GigaSpaces等。你可以很容易的实现会话集群而无需关注底层的容器实现。
  4. 异构客户端的访问:可以实现web中的session和非web项目中的session共享。
  5. 会话事件监听:提供对对session整个生命周期的监听。
  6. 保存主机地址:在会话开始session会存用户的ip地址和主机名,以此可以判断用户的位置。
  7. 会话失效/过期的支持:用户长时间处于不活跃状态可以使会话过期,调用touch()方法,可以主动更新最后访问时间,让会话处于活跃状态。
  8. 透明的Web支持:shiro全面支持Servlet 2.5中的session规范。这意味着你可以将你现有的web程序改为shiro会话,而无需修改代码。
  9. 单点登录的支持:shiro session基于普通java对象,使得它更容易存储和共享,可以实现跨应用程序共享。可以根据共享的会话,来保证认证状态到另一个程序。从而实现单点登录。

常用api

Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();

与web中的 HttpServletRequest.getSession(boolean create) 类似!
Subject.getSession(true)。即如果当前没有创建session对象会创建一个;
Subject.getSession(false),如果当前没有创建session对象则返回null。

实现session管理、共享

注意:session存入redis中,用户类User需要序列化。

1、增加redis依赖,增加redis配置信息,shiro配置信息

<dependency>
    <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-redis</artifactId>
 </dependency>
spring:
  redis:
    host: www.kaicostudy.com
    port: 6379
    database: 0

shiro:
  jessionid: kaico
  loginUrl: /login
  session:
    expireTime: 43200000

2、增加session dao层操作,实线将session存储到redis中

@Service
@Log4j2
public class RedisSessionDao extends AbstractSessionDAO {
   // Session超时时间,单位为毫秒
    @Value("${shiro.session.expireTime}")
    private Long expireTime;

    @Autowired
    private RedisTemplate redisTemplate;// Redis操作类,对这个使用不熟悉的,可以参考前面的博客

    public RedisSessionDao() {
        super();
    }

    public RedisSessionDao(long expireTime, RedisTemplate redisTemplate) {
        super();
        this.expireTime = expireTime;
        this.redisTemplate = redisTemplate;
    }

    @Override // 更新session
    public void update(Session session) throws UnknownSessionException {
        log.info("===============update================");
        if (session == null || session.getId() == null) {
            return;
        }
        session.setTimeout(expireTime);
        redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.MILLISECONDS);
    }

    @Override // 删除session
    public void delete(Session session) {
        log.info("===============delete================");
        if (null == session) {
            return;
        }
        redisTemplate.opsForValue().getOperations().delete(session.getId());
    }

    @Override
// 获取活跃的session,可以用来统计在线人数,如果要实现这个功能,可以在将session加入redis时指定一个session前缀,统计的时候则使用keys("session-prefix*")的方式来模糊查找redis中所有的session集合
    public Collection<Session> getActiveSessions() {
        log.info("==============getActiveSessions=================");
        return redisTemplate.keys("*");
    }

    @Override// 加入session
    protected Serializable doCreate(Session session) {
        log.info("===============doCreate================");
        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);

        redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.MILLISECONDS);
        return sessionId;
    }

    @Override// 读取session
    protected Session doReadSession(Serializable sessionId) {
        log.info("==============doReadSession=================");
        if (sessionId == null) {
            return null;
        }
        return (Session) redisTemplate.opsForValue().get(sessionId);
    }

    public long getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(long expireTime) {
        this.expireTime = expireTime;
    }

    public RedisTemplate getRedisTemplate() {
        return redisTemplate;
    }

    public void setRedisTemplate(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;

    }
}

3、将重写的RedisSessionDao 接入到shiro 中的sessionManager,shiro配置类代码

@Log4j2
@Configuration
public class ShiroConfig {

    @Value("${shiro.loginUrl}")
    private String loginUrl;

    @Value("${shiro.jessionid}")
    private String jessionId;
    
		@Value("${shiro.session.expireTime}")
    private Long expireTime;

    //Filter工厂,设置对应的过滤条件和跳转条件
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        //登出
        map.put("/logout", "logout");
        //对所有用户认证
        map.put("/**", "authc");
        //登录
        log.info("loginUrl:" + loginUrl);
        shiroFilterFactoryBean.setLoginUrl(loginUrl);
//        //首页
//        shiroFilterFactoryBean.setSuccessUrl("/index");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }


    /**
     * 权限管理,配置主要是Realm的管理认证
     * 创建DefaultWebSecurityManager
     * @return
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        //将sessionManager 注入到securityManager
        securityManager.setSessionManager(defaultWebSessionManager());
        return securityManager;
    }

    /**
     * 创建Realm
     */

    @Bean
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();

        // 设置加密算法
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher("md5");
        // 设置加密次数
        credentialsMatcher.setHashIterations(1);
        userRealm.setCredentialsMatcher(credentialsMatcher);

        return userRealm;
    }

    /**
     * 开启Shiro注解(如@RequiresRoles,@RequiresPermissions),
     * 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 开启aop注解支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }


    /**
     * 给shiro的sessionId默认的JSSESSIONID名字改掉
     * @return
     */
    @Bean(name="sessionIdCookie")
    public SimpleCookie getSessionIdCookie(){
        SimpleCookie simpleCookie = new SimpleCookie(jessionId);
        return simpleCookie;
    }
    @Bean
    public RedisSessionDao getRedisSessionDao(){
        return new RedisSessionDao();
    }
    /**
     * @see DefaultWebSessionManager
     * @return
     */
    @Bean(name="sessionManager")
    public DefaultWebSessionManager defaultWebSessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        //sessionManager.setCacheManager(cacheManager());
       sessionManager.setGlobalSessionTimeout(expireTime);
        sessionManager.setDeleteInvalidSessions(true);
        //关键在这里
        sessionManager.setSessionDAO(getRedisSessionDao());
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionIdCookie(getSessionIdCookie());
        return sessionManager;
    }
}

实现shiro缓存

实现缓存的作用,这样每次请求接口不用走realm权限认证的方法了。提高效率。

工具类

@Component
public class ApplicationContextUtil implements ApplicationContextAware {
 
    private static ApplicationContext context;
 
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }
 
    //获取上下文路径
    public static ApplicationContext getContext(){
        return context;
    }
 
    //获取bean
    public static Object getBean(String beanName){
        return context.getBean(beanName);
    }
}

//解决shiro盐加密的序列化问题
public class ByteSourceUtils {

    public static ByteSource bytes(byte[] bytes) {

        return new SimpleByteSource(bytes);

    }

    public static ByteSource bytes(String arg0) {

        return new SimpleByteSource(arg0.getBytes());

    }

}

redis缓存类,实现shiro的 Cache 接口,使用redis实现缓存

@Log4j2
public class RedisCache<K,V> implements Cache<K,V> {
    private String name ;
 
    public RedisCache(){
 
    }
 
    public RedisCache(String name){
        log.info("name="+name);
        this.name = name;
    }
 
    private RedisTemplate getRedisTemplate(){
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
 
    @Override
    public V get(K k) throws CacheException {
        log.info("------------------get from "+k.toString());
        return (V) getRedisTemplate().opsForHash().get(name,k.toString());
    }
 
    @Override
    public V put(K k, V v) throws CacheException {
        log.info("------------------put "+ v +" with "+k.toString());
        getRedisTemplate().opsForHash().put(name,k.toString(),v);
        return null;
    }
 
    @Override
    public V remove(K k) throws CacheException {
        log.info("------------------delete "+k.toString());
        getRedisTemplate().opsForHash().delete(name,k.toString());
        return null;
    }
 
    @Override
    public void clear() throws CacheException {
        log.info("------------------clear");
        getRedisTemplate().opsForHash().delete(name);
    }
 
    @Override
    public int size() {
        return 0;
    }
 
    @Override
    public Set<K> keys() {
        return getRedisTemplate().opsForHash().keys(this.name);
    }
 
    @Override
    public Collection<V> values() {
        return getRedisTemplate().opsForHash().values(this.name);
    }
}

RedisCacheManager 管理器

public class RedisCacheManager implements CacheManager {
    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        //返回自定义的缓存实现
        return new RedisCache<K,V>(s);
    }
}

由于shiro中提供的simpleByteSource实现,没有实现序列化,所以在认证时出现错误信息

public class SimpleByteSource extends org.apache.shiro.util.SimpleByteSource implements Serializable {

    private static final long serialVersionUID = 5528101080905698238L;

    public SimpleByteSource(byte[] bytes) {
        super(bytes);
    }
}

realm认证时,传入的盐用 MyByteSource 包装

/**
     * 认证逻辑
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行认证逻辑");
        // 编写shiro判断逻辑,判断用户名和密码
        System.out.println(authenticationToken.toString());
        // 1. 判断用户名
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        User byUsername = userService.getByUsername(username);
        if (byUsername == null) {
            //用户名不存在
            return null;//shiro底层会抛出UnknownAccountException
        }
        // 2. 判断密码
        // 参数1:需要返回给login方法的数据;参数2:数据库密码,shiro会自动判断 参数3:密码加密的盐
        return new SimpleAuthenticationInfo(byUsername, byUsername.getPassword(), ByteSourceUtils.bytes(username) , getName());
    }

修改shiro配置类 的ream bean配置

 @Bean
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();

        // 设置加密算法
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher("md5");
        // 设置加密次数
        credentialsMatcher.setHashIterations(1);
        userRealm.setCredentialsMatcher(credentialsMatcher);

        //开启缓存,设置缓存管理器
        userRealm.setCachingEnabled(true);
        userRealm.setAuthenticationCachingEnabled(true);
        userRealm.setAuthorizationCachingEnabled(true);
        userRealm.setCacheManager(new RedisCacheManager());

        return userRealm;
    }

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号