不用 Spring Security 可否?试试这个小而美的安全框架(下)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 不用 Spring Security 可否?试试这个小而美的安全框架(下)

授权


身份认证是验证你是谁的问题,而授权是你能干什么的问题,


产品经理:申购模块只能科室看程序员:好的产品经理:科长权限大一些,他也能看申购模块程序员:好的(黑脸)产品经理:科长不但能看,还能修改数据程序员:关公提大刀,拿命来…


作为程序员,我们的宗旨是:「能动手就不吵吵」; 硝烟怒火拔地起,耳边响起驼铃声(Shiro):「放下屠刀,立地成佛」授权没有那么麻烦,大家好商量…


整个过程和身份认证基本是一毛一样,你对比看看


角色实体创建


涉及到授权,自然要和角色相关,所以我们创建 Role 实体:


@Data
@Entity
public class Role {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    @Column(unique =true)
    private String roleCode;
    private String roleName;
}


新建 Role Repository


@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
    @Query(value = "select roleId from UserRoleRel ur where ur.userId = ?1")
    List<Long> findUserRole(Long userId);
    List<Role> findByIdIn(List<Long> ids);
}


定义权限实体 Permission


@Data
@Entity
public class Permission {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    @Column(unique =true)
    private String permCode;
    private String permName;
}


定义 Permission Repository


@Repository
public interface PermissionRepository extends JpaRepository<Permission, Long> {
    @Query(value = "select permId from RolePermRel pr where pr.roleId in ?1")
    List<Long> findRolePerm(List<Long> roleIds);
    List<Permission> findByIdIn(List<Long> ids);
}


建立用户与角色关系


其实可以通过 JPA 注解来制定关系的,这里为了说明问题,以单独外键形式说明


@Data
@Entity
public class UserRoleRel {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    private Long userId;
    private Long roleId;
}


建立角色与权限关系


@Data
@Entity
public class RolePermRel {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    private Long permId;
    private Long roleId;
}


编写 UserController


@RequiresPermissions("user:list:view")
@GetMapping()
public void getAllUsers(){
    List<User> users = userRepository.findAll();
}


@RequiresPermissions("user:list:view") 注解说明具有用户:列表:查看权限的才可以访问),官网明确给出权限定义格式,包括通配符等,我希望你自行去查看

自定义 CustomRealm (主要重写 doGetAuthorizationInfo) 方法:


微信图片_20220509202032.png


与认证流程如出一辙,只不过多了用户,角色,权限的关系罢了


授权流程说明


这里通过过滤器(见Shiro配置)和注解二者结合的方式来进行授权,和认证流程一样,最终会走到我们自定义的 CustomRealm 中,同样 Shiro 默认提供了许多注解用来处理不同的授权情况


注解 功能
@RequiresGuest 只有游客可以访问
@RequiresAuthentication 需要登录才能访问
@RequiresUser 已登录的用户或“记住我”的用户能访问
@RequiresRoles 已登录的用户需具有指定的角色才能访问
@RequiresPermissions 已登录的用户需具有指定的权限才能访问(如果不想和产品经理华山论剑,推荐用这个注解)


授权官网给出明确的授权策略与案例,请查看:http://shiro.apache.org/permi...

上面的例子我们通过一直在通过访问 Mysql 获取用户认证和授权信息,这中方式明显不符合生产环境的需求


Session会话管理


做过 Web 开发的同学都知道 Session 的概念,最常用的是 Session 过期时间,数据在 Session 的 CRUD,同样看上图,我们需要关注 SessionManager 和 SessionDAO 模块,Shiro starter 已经提供了基本的 Session配置信息,我们按需在YAML中配置就好(官网https://shiro.apache.org/spri... 已经明确给出Session的配置信息)


Key Default Value Description
shiro.enabled true Enables Shiro’s Spring module
shiro.web.enabled true Enables Shiro’s Spring web module
shiro.annotations.enabled true Enables Spring support for Shiro’s annotations
shiro.sessionManager.deleteInvalidSessions true Remove invalid session from session storage
shiro.sessionManager.sessionIdCookieEnabled true Enable session ID to cookie, for session tracking
shiro.sessionManager.sessionIdUrlRewritingEnabled true Enable session URL rewriting support
shiro.userNativeSessionManager false If enabled Shiro will manage the HTTP sessions instead of the container
shiro.sessionManager.cookie.name JSESSIONID Session cookie name
shiro.sessionManager.cookie.maxAge -1 Session cookie max age
shiro.sessionManager.cookie.domain null Session cookie domain
shiro.sessionManager.cookie.path null Session cookie path
shiro.sessionManager.cookie.secure false Session cookie secure flag
shiro.rememberMeManager.cookie.name rememberMe RememberMe cookie name
shiro.rememberMeManager.cookie.maxAge one year RememberMe cookie max age
shiro.rememberMeManager.cookie.domain null RememberMe cookie domain
shiro.rememberMeManager.cookie.path null RememberMe cookie path
shiro.rememberMeManager.cookie.secure false RememberMe cookie secure flag
shiro.loginUrl /login.jsp Login URL used when unauthenticated users are redirected to login page
shiro.successUrl / Default landing page after a user logs in (if alternative cannot be found in the current session)
shiro.unauthorizedUrl null Page to redirect user to if they are unauthorized (403 page)


分布式服务中,我们通常需要将Session信息放入Redis中来管理,来应对高并发的访问需求,这时只需重写SessionDAO即可完成自定义的Session管理


整合Redis


@Configuration
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    @Bean
    public RedisTemplate<String, Object> stringObjectRedisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}


重写SessionDao


微信图片_20220509202126.png


查看源码,可以看到调用默认SessionManager的retriveSession方法,我们重写该方法,将Session放入HttpRequest中,进一步提高session访问效率


微信图片_20220509202155.png


向ShiroConfig中添加配置


其实在概览模块已经给出代码展示,这里单独列出来做说明:


/**
 * 自定义RedisSessionDao用来管理Session在Redis中的CRUD
 * @return
 */
@Bean(name = "redisSessionDao")
public RedisSessionDao redisSessionDao(){
    return new RedisSessionDao();
}
/**
 * 自定义SessionManager,应用自定义SessionDao
 * @return
 */
@Bean(name = "customerSessionManager")
public CustomerWebSessionManager customerWebSessionManager(){
    CustomerWebSessionManager customerWebSessionManager = new CustomerWebSessionManager();
    customerWebSessionManager.setSessionDAO(redisSessionDao());
    return customerWebSessionManager;
}
/**
 * 定义Security manager
 * @param customRealm
 * @return
 */
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(CustomRealm customRealm) {
    DefaultWebSecurityManager  securityManager = new DefaultWebSecurityManager ();
    securityManager.setRealm(customRealm);
    securityManager.setSessionManager(customerWebSessionManager()); // 可不指定,Shiro会用默认Session manager
    securityManager.setCacheManager(redisCacheManagers());  //可不指定,Shiro会用默认CacheManager
//        securityManager.setSessionManager(defaultWebSessionManager());
    return securityManager;
}
/**
 * 定义session管理器
 * @return
 */
@Bean(name = "sessionManager")
public DefaultWebSessionManager defaultWebSessionManager(){
    DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
    defaultWebSessionManager.setSessionDAO(redisSessionDao());
    return defaultWebSessionManager;
}


至此,将 session 信息由 redis 管理功能就这样完成了


缓存管理


应对分布式服务,对于高并发访问数据库权限内容是非常低效的方式,同样我们可以利用Redis来解决这一问题,将授权数据缓存到Redis中


新建 RedisCache


@Slf4j
@Component
public class RedisCache<K, V> implements Cache<K, V> {
    public static final String SHIRO_PREFIX = "shiro-cache:";
    @Resource
    private RedisTemplate<String, Object> stringObjectRedisTemplate;
    private String getKey(K key){
        if (key instanceof String){
            return (SHIRO_PREFIX + key);
        }
        return key.toString();
    }
    @Override
    public V get(K k) throws CacheException {
        log.info("read from redis...");
        V v = (V) stringObjectRedisTemplate.opsForValue().get(getKey(k));
        if (v != null){
            return v;
        }
        return null;
    }
    @Override
    public V put(K k, V v) throws CacheException {
        stringObjectRedisTemplate.opsForValue().set(getKey(k), v);
        stringObjectRedisTemplate.expire(getKey(k), 100, TimeUnit.SECONDS);
        return v;
    }
    @Override
    public V remove(K k) throws CacheException {
        V v = (V) stringObjectRedisTemplate.opsForValue().get(getKey(k));
        stringObjectRedisTemplate.delete((String) get(k));
        if (v != null){
            return v;
        }
        return null;
    }
    @Override
    public void clear() throws CacheException {
        //不要重写,如果只保存shiro数据无所谓
    }
    @Override
    public int size() {
        return 0;
    }
    @Override
    public Set<K> keys() {
        return null;
    }
    @Override
    public Collection<V> values() {
        return null;
    }
}


新建 RedisCacheManager


public class RedisCacheManager implements CacheManager {
    @Resource
    private RedisCache redisCache;
    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return redisCache;
    }
}


至此,我们不用每次访问 Mysql DB 来获取认证和授权信息,而是通过 Redis 来缓存这些信息,大大提升了效率,也满足分布式系统的设计需求


总结


回复获取 demo 代码。这里只是梳理了Springboot整合Shiro的流程,以及应用Redis最大化利用Shiro,Shiro的使用细节还很多,官网说的也很明确,带着上面的架构图来理解Shiro会事半功倍,感觉这里面的代码挺多挺头大的?那是你没有自己动手去尝试,结合官网与 demo 相信你会对 Shiro 有更好的理解,另外你可以理解 Shiro 是 mini 版本的 Spring Security,我希望以小见大,当需要更细粒度的认证授权时,也会对理解 Spring Security 有很大帮助,点击文末「阅读原文」,效果更好


落霞与孤鹜齐飞 秋水共长天一色,产品经理和程序员一片祥和…


灵魂追问


  1. 都说 Redis 是单线程,但是很快,你知道为什么吗?


  1. 你们项目中是怎样控制认证授权的呢?当授权有变化,对于程序员来说,这个修改是灾难吗?
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
13天前
|
安全 Java 开发者
如何在Spring框架中实现横切关注点的集中管理和重用?
【4月更文挑战第30天】如何在Spring框架中实现横切关注点的集中管理和重用?
18 0
|
13天前
|
安全 Java 程序员
Spring框架的核心特性是什么?
【4月更文挑战第30天】Spring 的特性
17 0
|
3天前
|
XML Java 数据库连接
Spring框架与Spring Boot的区别和联系
Spring框架与Spring Boot的区别和联系
13 0
|
5天前
|
前端开发 安全 Java
使用Spring框架加速Java开发
使用Spring框架加速Java开发
14 0
|
5天前
|
设计模式 数据采集 监控
Spring日志框架
Spring日志框架
8 0
|
5天前
|
前端开发 Java 应用服务中间件
Spring MVC框架概述
Spring MVC 是一个基于Java的轻量级Web框架,采用MVC设计模型实现请求驱动的松耦合应用开发。框架包括DispatcherServlet、HandlerMapping、Handler、HandlerAdapter、ViewResolver核心组件。DispatcherServlet协调这些组件处理HTTP请求和响应,Controller处理业务逻辑,Model封装数据,View负责渲染。通过注解@Controller、@RequestMapping等简化开发,支持RESTful请求。Spring MVC具有清晰的角色分配、Spring框架集成、多种视图技术支持以及异常处理等优点。
12 1
|
13天前
|
SQL Java 数据库连接
Springboot框架整合Spring JDBC操作数据
JDBC是Java数据库连接API,用于执行SQL并访问多种关系数据库。它包括一系列Java类和接口,用于建立数据库连接、创建数据库操作对象、定义SQL语句、执行操作并处理结果集。直接使用JDBC涉及七个步骤,包括加载驱动、建立连接、创建对象、定义SQL、执行操作、处理结果和关闭资源。Spring Boot的`spring-boot-starter-jdbc`简化了这些步骤,提供了一个在Spring生态中更便捷使用JDBC的封装。集成Spring JDBC需要添加相关依赖,配置数据库连接信息,并通过JdbcTemplate进行数据库操作,如插入、更新、删除和查询。
|
13天前
|
SQL Java 数据库连接
Springboot框架整合Spring Data JPA操作数据
Spring Data JPA是Spring基于ORM和JPA规范封装的框架,简化了数据库操作,提供增删改查等接口,并可通过方法名自动生成查询。集成到Spring Boot需添加相关依赖并配置数据库连接和JPA设置。基础用法包括定义实体类和Repository接口,通过Repository接口可直接进行数据操作。此外,JPA支持关键字查询,如通过`findByAuthor`自动转换为SQL的`WHERE author=?`查询。
|
13天前
|
安全 Java 开发者
在Spring框架中,IoC和AOP是如何实现的?
【4月更文挑战第30天】在Spring框架中,IoC和AOP是如何实现的?
22 0
|
13天前
|
Java API 数据安全/隐私保护
【亮剑】如何在Java项目中结合Spring框架实现邮件发送功能
【4月更文挑战第30天】本文介绍了如何在Java项目中结合Spring框架实现邮件发送功能。首先,需在`pom.xml`添加Spring和JavaMail依赖。然后,在`applicationContext.xml`配置邮件发送器,包括SMTP服务器信息。接着,创建一个使用依赖注入的`EmailService`类,通过`JavaMailSender`发送邮件。最后,调用`EmailService`的`sendSimpleEmail`方法即可发送邮件。最佳实践包括:使用配置管理敏感信息,利用`MimeMessage`构造复杂邮件,异常处理和日志记录,以及在大量发送时考虑使用邮件队列。