spring security 结合数据库实战 (三)

一、这节开始,我们通过mysql存储用户信息(springboot)

1、引入jpa和mysql依赖,同时配置mysql

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>

配置:

spring.datasource.url=jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT
spring.datasource.username=root
spring.datasource.password=xxx
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

2、设计表

涉及到三张表:用户表,角色表,用户角色关联表,表设计如下:

CREATE TABLE IF NOT EXISTS `user`
(
	`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',
	`username` varchar(128) NOT NULL,
	`password` varchar(128) NOT NULL,
	`create_time` datetime NOT NULL,
	PRIMARY KEY (`id`),
	UNIQUE KEY (`username`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
ALTER TABLE user CHANGE username user_name varchar(128) NOT NULL;

CREATE TABLE IF NOT EXISTS `role`
(
	`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',
	`role_name` varchar(128) NOT NULL,
	PRIMARY KEY (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `user_role`
(
        `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',
	`user_id` bigint(11) NOT NULL ,
	`role_id` bigint(11) NOT NULL
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

插入测试数据(密码这里都是使用了BCryptPasswordEncoder 需在SecurityConfig中加入配置):

//密码为123456
insert into user (`user_name`,`password`,`create_time`) values('testuser',' $2a$10$0HHE8ldG4hnC.sVGUFNdB.sVFf.kSvKZU21y7mPyqo3hC2xducvL2','2019-3-1'),('testadmin',' $2a$10$0HHE8ldG4hnC.sVGUFNdB.sVFf.kSvKZU21y7mPyqo3hC2xducvL2','2019-3-1');

insert into role(`role_name`) values('ADMIN'),('USER');

insert into user_role(`user_id`,`role_id`) values('1','2'),('2','1'),('2','2');

3、三个表对应实体类(注意: 可以通过 @Entity @Column 等注解自动生成上面的表)

@Data
@Entity
public class User implements Serializable {
    @Id
    private Long id;
    private String userName;
    private String password;
    @JsonFormat(timezone="GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
}

@Data
@Entity
public class Role implements Serializable {
    @Id
    private Long id;
    private String roleName;
}

@Data
@Entity
public class UserRole {
    @Id
    private Long id;
    private Long userId;
    private Long roleId;
}

实体类通过lombok注解省去get/set方法,这里不细讲,引入lombok依赖。

     <dependency>
         <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
         <version>1.16.6</version>
      </dependency>

4、User实体类对应的jpa接口(其他忽略)

public interface UserRepository extends JpaRepository<User,Long> {
    User findOneByUserName(String userName);
}

5、spring security 通过调用 UserDetailsService 实现类的 loadUserByUsername 方法获取到UserDetails 实现类,从而进行用户的身份认证,因此我们需要自定义 UserDetailsService 接口对应的实现类完成相应获取用户逻辑,这是自定义用户功能的核心。

Authentication接口才是真正使用的安全验证对象(有未认证、已认证两种状态),UserDetails 是用户安全信息的源,security会把 UserDetails 与 Authentication 进行匹配,成功后将用户信息拷贝到 Authentication中,最后Authentication把身份信息与其他组件共享。 另外Authentication实现类都保存了一个GrantedAuthority列表,该列表表示用户所具有的权限。GrantedAuthority是通过AuthenticationManager设置到Authentication对象中的,然后AccessDecisionManager将从Authentication中获取用户所具有的GrantedAuthority来鉴定用户是否具有访问对应资源的权限。

@Component
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {

        //1.根据用户名查找用户
        User user = userRepository.findOneByUserName(name); 

        if (Objects.isNull(user)){
            //注意,这是要求的,找不到抛异常
            throw new UsernameNotFoundException(name + "not found");
        }

        //2、表关联查询获取用户对应的角色信息,这里忽略,可以写mapper在查用户的时候一次性获取
        List<Role> roles = xxxxx;

        // 2. 将用户拥有的权限加到 grantedAuthorities(此处),注意我们在所有权限前面加了'ROLE_'字符串,这是因为下面 MySecurityConfig 类中对资源加权限 hasRole("ADMIN") 方法中会为加入的字符串前面统一加上"ROLE_",可以看源码
        Collection<GrantedAuthority> grantedAuthorities = roles.stream().map(r -> {
            GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + r.getRoleName());
            return grantedAuthority;
        }).collect(Collectors.toList());
        //Collection<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
        //3、按要求返回security对应的User,其实现了UserDetails
        return new org.springframework.security.core.userdetails.User(name,
                user.getPassword(), grantedAuthorities);
    }
}

6、在 WebSecurityConfigurerAdapter 对应实现类 SecurityConfig 加入 MyUserDetailsService 配置,同时还要自定义 PasswordEncoder(BCryptPasswordEncoder) 实现对密码的加密支持

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 设置自定义的userDetailsService
        auth.userDetailsService(myUserDetailsService)
                //也可自定义实现 PasswordEncoder 接口
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    //处理密码的问题,生成密码可以使用PasswordEncoder 的encode方法,主要处理加密/密码匹配等
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
                // 在UsernamePasswordAuthenticationFilter 过滤器前加自定义过滤器
                //validateCodeFilter 是我们自己实现的过滤器,该过滤器继承 OncePerRequestFilter ,保证一个请求该过滤器只被调用一次
                //http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
        http
                .authorizeRequests()
                .antMatchers("/user/**").hasRole("USER")
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin().and()
                .httpBasic();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/filter", "/js/**", "/css/**");
    }
}

7、至此,我们就实现了从数据库获取用户信息并整合到spring security的功能。

8、另外,可能部署到生成环境后,会出现希望 /user/** 对应的接口改成只给USER角色的用户访问的需求。 所以一般角色管理比较复杂的项目,一般还会有两个数据表,一个存放应用提供的接口表permission(主要含 id、url、资源描述信息如菜单等字段) 以及角色与访问资源对应的关联关系表role_permission(主要含 role_id、permission_id)。

其实RBAC的通用数据模型都是五张表:用户表、角色表、资源表、用户与角色关联表、角色与资源关联表,其中用户表与角色表n对n,角色表与资源表也是n对n关系。

(1)创建实现  FilterInvocationSecurityMetadataSource 的类来储存请求资源与权限的对应关系。

@Component
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    //定义map存放访问资源与需要的角色Collection<ConfigAttribute>决策器 的关系
    private static HashMap<String, Collection<ConfigAttribute>> map =null;

    @Override
    //当接收到http请求时, filterSecurityInterceptor会调用的方法。这个方法返回请求该url所需要的所有权限集合。
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        //初始化 访问资源(url)与角色的对应关系(即将角色与资源关联表role_permission的所有数据取出,加到权限设置)
        if (map == null){
            map = new HashMap<>();
            List<RolePermisson> rps = xxx;  //获取关系表的全部内容
            //遍历全部关联信息,取出对应的 url 和 角色信息(某个资源可以被哪个角色访问)
            rps.stream().forEach(rp -> {
                String roleId = rp.getRoleId();
                String permissionId = rp.getPermissionId();
                String roleName = xxx;  //通过roleId 找出roleName;
                String url = xxx; //通过permissionId 找出访问url
                ConfigAttribute configAttribute = new SecurityConfig(roleName);
                //url是否加过别的角色
                if (map.containsKey(url)){
                    map.get(url).add(configAttribute);
                } else{
                    List<ConfigAttribute> configAttributes = new ArrayList<>();
                    configAttributes.add(configAttribute);
                    map.put(url, configAttributes);
                }
            });
        }
        //object 中包含用户请求的request 信息
        HttpServletRequest request = ((FilterInvocation) o).getHttpRequest();
        for (Iterator<String> it = map.keySet().iterator(); it.hasNext();) {
            String url = it.next();
            if (new AntPathRequestMatcher(url).matches(request)) {
                return map.get(url);
            }
        }
        return null;
    }

    @Override
    //Spring容器启动时自动调用, 一般把所有请求与权限的对应关系也要在这个方法里初始化, 保存在一个属性变量里。
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    //该类是否能够为某资源提供ConfigAttributes。
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

(2)创建实现  AccessDecisionManager 的类来负责鉴定用户是否有访问对应资源(方法或URL)的权限,其由AbstractSecurityInterceptor调用的。(决策器)

@Component
 class MyAccessDecisionManager implements AccessDecisionManager {
    /**
     * 通过参数来决定用户是否有访问对应资源的权限
     * @param authentication 含当前用户信息,以及拥有的权限。权限来源于前面登录时UserDetailsService中设置的authorities。
     * @param o  FilterInvocation对象,可以得到request
     * @param collection configAttributes是本次访问需要的权限
     */
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        if (Objects.isNull(collection) || collection.isEmpty()) {
            return;
        } else {
            collection.stream().forEach(c -> {
                String shouldRole = c.getAttribute();
                authentication.getAuthorities().stream().forEach(a -> {
                    //有访问权限,直接return
                    if (shouldRole .trim().equals(((GrantedAuthority) a).getAuthority().trim())){
                        return;
                    }
                });
            });
            throw new AccessDeniedException("当前访问没有权限");
        }
    }

    @Override
    //表示此AccessDecisionManager是否能够处理传递的ConfigAttribute呈现的授权请求
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    //本类是否能够为指定的资源提供访问控制决策
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

(3)创建继承 AbstractSecurityInterceptor 的类,AbstractSecurityInterceptor 是一个实现了对受保护资源的访问进行拦截的抽象类。这里主要就是为了使用我们之前自定义的 MyAccessDecisionManager 和 MyFilterInvocationSecurityMetadataSource。

@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements  Filter{

    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    @Autowired
    public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return securityMetadataSource;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            //执行下一个拦截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }
}

AbstractSecurityInterceptor的机制可以分为几个步骤:

  1. 查找与当前请求关联的“配置属性(简单的理解就是权限)”
  2. 将 安全对象(方法调用或Web请求)、当前身份验证、配置属性 提交给决策器(AccessDecisionManager)
  3. (可选)更改调用所根据的身份验证
  4. 允许继续进行安全对象调用(假设授予了访问权)
  5. 在调用返回之后,如果配置了AfterInvocationManager。如果调用引发异常,则不会调用AfterInvocationManager。

AbstractSecurityInterceptor中的方法说明:

  • beforeInvocation()方法实现了对访问受保护对象的权限校验,内部用到了AccessDecisionManager和AuthenticationManager;
  • finallyInvocation()方法用于实现受保护对象请求完毕后的一些清理工作,主要是如果在beforeInvocation()中改变了SecurityContext,则在finallyInvocation()中需要将其恢复为原来的SecurityContext,该方法的调用应当包含在子类请求受保护资源时的finally语句块中。
  • afterInvocation()方法实现了对返回结果的处理,在注入了AfterInvocationManager的情况下默认会调用其decide()方法。

9、默认情况下,其实 spring security 使用到了session,即身份认证信息是通过session和cookie实现的。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值