当我们在OAuth登陆后,获取了登陆的令牌,使用该令牌,我们就有了访问一些受OAuth保护的接口的能力。具体可以看本人的这两篇博客OAuth2.0用户名,密码登录解析 OAuth2.0通过token获取受保护资源的解析
但现在我们要区分这些登陆人员的具体分工,哪些接口归哪些登陆人员可以访问,这就要用到了spring security中的权限控制。
首先我们需要有一个权限的对象
/** * 权限标识 */ @Data public class SysPermission implements Serializable { private static final long serialVersionUID = 280565233032255804L; private Long id; //权限id private String permission; //具体的权限 private String name; //权限名称 private Date createTime; private Date updateTime; }
对应于数据库中的权限表
那么问题来了,我们要对权限进行管理需要什么样的权限呢,当然我们需要权限管理权限,这是在系统一开始建立的时候保存进数据库的
这四个权限并不是通过前端写入的。
现在我们需要通过前端接口增加其他的权限就需要使用到这四个权限之一。
在这里我们给出一些权限的增删改查的mybatis dao
@Mapper public interface SysPermissionDao { @Options(useGeneratedKeys = true, keyProperty = "id") @Insert("insert into sys_permission(permission, name, createTime, updateTime) values(#{permission}, #{name}, #{createTime}, #{createTime})") int save(SysPermission sysPermission); @Update("update sys_permission t set t.name = #{name}, t.permission = #{permission}, t.updateTime = #{updateTime} where t.id = #{id}") int update(SysPermission sysPermission); @Delete("delete from sys_permission where id = #{id}") int delete(Long id); @Select("select * from sys_permission t where t.id = #{id}") SysPermission findById(Long id); @Select("select * from sys_permission t where t.permission = #{permission}") SysPermission findByPermission(String permission); int count(Map<String, Object> params); List<SysPermission> findData(Map<String, Object> params); }
现在我们要在Controller中增加一个新的权限
/** * 管理后台添加权限 * * @param sysPermission * @return */ @LogAnnotation(module = LogModule.ADD_PERMISSION) @PreAuthorize("hasAuthority('back:permission:save')") @PostMapping("/permissions") public SysPermission save(@RequestBody SysPermission sysPermission) { if (StringUtils.isBlank(sysPermission.getPermission())) { throw new IllegalArgumentException("权限标识不能为空"); } if (StringUtils.isBlank(sysPermission.getName())) { throw new IllegalArgumentException("权限名不能为空"); } sysPermissionService.save(sysPermission); return sysPermission; }
我们可以看到这个标签@PreAuthorize("hasAuthority('back:permission:save')"),首先我们是通过access_token令牌访问的该接口,系统可以知道登陆的是哪一个用户,以此看看该用户是否有back:permission:save的访问权限
我们来看看用户角色
@Data public class SysRole implements Serializable { private static final long serialVersionUID = -2054359538140713354L; private Long id; //角色id private String code; //角色编码 private String name; //角色名称 private Date createTime; private Date updateTime; }
对应数据库中的表结构如下
并给定一个管理员角色
该角色对应于哪些权限,这里可以看到是所有权限
而我们的用户是哪个角色呢
我们可以看到这里有两个用户,他们都属于管理员角色
如果我们现在用其中的一个用户登陆,并获取该用户的信息如下
/** * 用于指定将计算为的方法访问控制表达式的批注 * 决定是否允许方法调用。 * * @author Luke Taylor * @since 3.0 */ @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface PreAuthorize { /** * @return 在执行这个受保护的方法前进行Spring EL表达式的解析 */ String value(); }
这里有一个Spring EL表达式都解析,我们来看一下什么是Spring EL表达式
public class SpringELTest { public static void main(String[] args) { ExpressionParser parser = new SpelExpressionParser(); //解析字符串,该字符串具有一段代码的味道 Expression expression = parser.parseExpression("'Hello World'.bytes.length"); int length = (int)expression.getValue(); System.out.println(length); } }
这个"'Hello World'.bytes.length"就是一段Spring EL表达式,虽然是一段字符串,但它有一段代码的含义,可以被解析执行
运行结果
11
那么很明显"hasAuthority('back:permission:save')"就是一段Spring EL表达式,它是可以被执行的。
要想使标签@PreAuthorize生效,我们需要设置一下OAuth的资源服务设置
/** * 资源服务配置 */ @EnableResourceServer @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable().exceptionHandling() .authenticationEntryPoint( (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) .and().authorizeRequests().antMatchers(PermitAllUrl.permitAllUrl("/users-anon/**", "/sys/login","/wechat/**")).permitAll() .anyRequest().authenticated().and().httpBasic(); } @Override public void configure(ResourceServerSecurityConfigurer resource) throws Exception { //这里把自定义异常加进去 resource.authenticationEntryPoint(new AuthExceptionEntryPoint()) .accessDeniedHandler(new CustomAccessDeniedHandler()); } @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } }
其中@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)就是打开@PreAuthorize进行拦截生效的标签,当然一定要设置prePostEnabled = true。
既然"hasAuthority('back:permission:save')"是一段Spring EL表达式,那么hasAuthority()就一定是一个可以执行的方法,该方法位于SecurityExpressionRoot类下,该类位于org.springframework.security.access.expression包中。
Spring Security可用表达式对象的基类就是SecurityExpressionRoot,它支持很多的方法
表达式 | 描述 |
hasRole([role]) | 当前用户是否拥有指定角色。 |
hasAnyRole([role1,role2]) | 多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。 |
hasAuthority([auth]) | 等同于hasRole |
hasAnyAuthority([auth1,auth2]) | 等同于hasAnyRole |
Principle | 代表当前用户的principle对象 |
authentication | 直接从SecurityContext获取的当前Authentication对象 |
permitAll | 总是返回true,表示允许所有的 |
denyAll | 总是返回false,表示拒绝所有的 |
isAnonymous() | 当前用户是否是一个匿名用户 |
isRememberMe() | 表示当前用户是否是通过Remember-Me自动登录的 |
isAuthenticated() | 表示当前用户是否已经登录认证成功了。 |
isFullyAuthenticated() | 如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录的,则返回true。 |
我们具体看一下hasAuthority这个方法的实现,只有当这个方法返回的结果为true的时候,我们才能进一步访问我们的接口代码
这里面传入的authority为"back:permission:save"
public final boolean hasAuthority(String authority) { return hasAnyAuthority(authority); }
public final boolean hasAnyAuthority(String... authorities) { return hasAnyAuthorityName(null, authorities); }
private boolean hasAnyAuthorityName(String prefix, String... roles) { //获取所有的用户角色权限 Set<String> roleSet = getAuthoritySet(); //由于我们这里传入的roles只有"back:permission:save" //所以role即为"back:permission:save",prefix则为null for (String role : roles) { //defaultedRole依然为"back:permission:save" String defaultedRole = getRoleWithDefaultPrefix(prefix, role); //在权限集合中是否包含"back:permission:save"的该权限 //根据我们之前登录的返回信息,可以看到"authority": "back:permission:save"的存在 //所以此处是可以通过权限验证的。 if (roleSet.contains(defaultedRole)) { return true; } } return false; }
private Set<String> getAuthoritySet() { //Set<String> roles是SecurityExpressionRoot的属性 //我们可以看到它是从一系列用户认证里面获取到的权限集合 if (roles == null) { roles = new HashSet<>(); //authentication是SecurityExpressionRoot极为重要的一个属性,它本身是一个接口 //管理着用户认证信息的各个方法 Collection<? extends GrantedAuthority> userAuthorities = authentication .getAuthorities(); if (roleHierarchy != null) { userAuthorities = roleHierarchy .getReachableGrantedAuthorities(userAuthorities); } roles = AuthorityUtils.authorityListToSet(userAuthorities); } return roles; }
private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) { if (role == null) { return role; } //由于defaultRolePrefix为null,所以此处返回的就是"back:permission:save" if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) { return role; } if (role.startsWith(defaultRolePrefix)) { return role; } return defaultRolePrefix + role; }
我们可以看一下AuthorityUtils.authorityListToSet()方法
public static Set<String> authorityListToSet( Collection<? extends GrantedAuthority> userAuthorities) { Set<String> set = new HashSet<>(userAuthorities.size()); //很明显这里是把认证用户的所有权限给转化为Set集合 for (GrantedAuthority authority : userAuthorities) { set.add(authority.getAuthority()); } return set; }