授权
身份认证是验证你是谁的问题,而授权是你能干什么的问题,
产品经理:申购模块只能科室看程序员:好的产品经理:科长权限大一些,他也能看申购模块程序员:好的(黑脸)产品经理:科长不但能看,还能修改数据程序员:关公提大刀,拿命来…
作为程序员,我们的宗旨是:「能动手就不吵吵」; 硝烟怒火拔地起,耳边响起驼铃声(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) 方法:
与认证流程如出一辙,只不过多了用户,角色,权限的关系罢了
授权流程说明
这里通过过滤器(见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
查看源码,可以看到调用默认SessionManager的retriveSession方法,我们重写该方法,将Session放入HttpRequest中,进一步提高session访问效率
向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 有很大帮助,点击文末「阅读原文」,效果更好
落霞与孤鹜齐飞 秋水共长天一色,产品经理和程序员一片祥和…
灵魂追问
- 都说 Redis 是单线程,但是很快,你知道为什么吗?
- 你们项目中是怎样控制认证授权的呢?当授权有变化,对于程序员来说,这个修改是灾难吗?