(二)基于数据库的认证与授权

环境准备

controller

@RestController
@RequestMapping("/api/admin")
public class AdminController {

    @GetMapping("/hello")
    public String hello(){
        return "hello! this is admin page";
    }
}

/**----------------------分割线------------------------*/

@RestController
@RequestMapping("/api/user")
public class UserController {

    @GetMapping("/hello")
    public String hello(){
        return "hello! this is user page!";
    }
}

/**----------------------分割线------------------------*/

@RestController
@RequestMapping("/api/public")
public class PublicController {

    @GetMapping("/hello")
    public String hello(){
        return "hello! this is public page";
    }
}

资源权限配置

@EnableWebSecurity
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/user/**").hasAnyRole("user")  //user 角色访问/api/user/开头的路由
                .antMatchers("/api/admin/**").hasAnyRole("admin") //admin角色访问/api/admin/开头的路由
                .antMatchers("/api/public/**").permitAll()                 //允许所有可以访问/api/public/开头的路由
                .and()
            .formLogin();
    }
}

antMatchers()是一个采用ANT模式的URL匹配器:

  • * 表示匹配0或任意数量的字符
  • ** 表示匹配0或者更多的目录。antMatchers("/admin/api/**")相当于匹配了/admin/api/下的所有API。

配置默认用户,密码和角色信息

#默认登录用户
spring.security.user.name=user
#默认user用户密码
spring.security.user.password=user
#默认user用户所属角色
spring.security.user.roles=user

启动服务进行验证

直接方访问localhost:8080/api/public/hello,没问题,正常访问:
在这里插入图片描述
访问localhost:8080/api/user/hello,跳转到了登录验证页面:
在这里插入图片描述
正确使用user用户进行登录后,也能够正常访问:
在这里插入图片描述
访问localhost:8080/api/admin/hello,跳转到了登录验证页面:

此时正确使用user用户进行登录后,发现提示了403
在这里插入图片描述

页面显示403错误,表示该用户授权失败,401代表该用户认证失败;本次访问已经通过了认证环节,只是在授权的时候被驳回了。

基于内存的多用户支持

到目前为止,我们仍然只有一个可登录的用户,怎样引入多用户呢?非常简单,我们只需实现一个自定义的UserDetailsService即可:

    @Bean
    public UserDetailsService userDetailsService(){
        UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        //创建用户user01,密码user01,角色user
        userDetailsManager.createUser(User.withUsername("user01").password("user01").roles("user").build());
        //创建用户admin01,密码admin01,角色admin
        userDetailsManager.createUser(User.withUsername("admin01").password("admin01").roles("admin").build());
        return userDetailsManager;
    }

其中InMemoryUserDetailsManagerUserDetailManager的实现类,它将用户数据源寄存在内存里,在一些不需要引入数据库这种重数据源的系统中很有帮助。
UserDetailManager 继承了 UserDetailService;

重启服务,使用新创建的用户admin01去访问localhost:8080/api/admin/hello,我们可以发现,依旧未能够正确认证并授权:
在这里插入图片描述

这块儿在陈木鑫老师的书中是已经能够正确认证并授权通过了,现在为什么没有成功呢?
因为Spring security 5.0中新增了多种加密方式,也改变了密码的格式。详见:https://blog.csdn.net/canon_in_d_major/article/details/79675033

我们这里针对性对userDetailsService进行修改,指明使用默认的passwordEncoder

    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();

        //创建用户user01,密码user01,角色user
        userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
        //创建用户admin01,密码admin01,角色admin
        userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());

        return userDetailsManager;
    }

这样我们就可以正确的认证访问了:
在这里插入图片描述

基于默认数据库模型的认证与授权

除了InMemoryUserDetailsManager,Spring Security还提供另一个UserDetailsService实现类:JdbcUserDetailsManager
JdbcUserDetailsManager帮助我们以JDBC的方式对接数据库和Spring Security。

JdbcUserDetailsManager设定了一个默认的数据库模型,只要遵从这个模型,在简便性上,JdbcUserDetailsManager甚至可以媲美InMemoryUserDetailsManager

准备数据库

我这里使用的是PostgreSQL数据库,在这块儿使用其他数据库(例如MySQL)都是一样的,这块儿看个人爱好吧。

(1)在工程中引入jdbcPostgreSQL两个必要依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>

(2)在application.properties配置文件中配置数据库连接信息:

#数据库连接信息
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/springsecuritydemo?schema=public
spring.datasource.data-username=postgres
spring.datasource.data-password=aaaaaa

(3)预制表结构和数据
JdbcUserDetailsManager设定了一个默认的数据库模型,Spring Security将该模型定义在/org/springframework/security/core/userdetails/jdbc/users.ddl内:

create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);
create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);

JdbcUserDetailsManager需要两个表,其中users表用来存放用户名、密码和是否可用三个信息,authorities表用来存放用户名及其权限的对应关系。

我们现在使用sql创建这两张表,在Pg数据库中执行上面的语句,我们发现以下错误:
在这里插入图片描述
因为该语句是用hsqldb创建的,而PostgreSQL不支持
varchar_ignorecase这种类型。怎么办呢?很简单,将varchar_ignorecase改为PostgreSQL支持的varchar即可:

create table users(username varchar(50) not null primary key,password varchar(500) not null,enabled boolean not null);
create table authorities (username varchar(50) not null,authority varchar(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
配置完成后,重启环境,正常启动。

编码实现

下面我们修改一下userDetailService bean,使用JdbcUserDetailsManager实现,让Spring Security使用数据库来管理用户:

    @Bean
    public UserDetailsService userDetailsService(DataSource dataSource){
        JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();
        userDetailsManager.setDataSource(dataSource);

        //创建用户user01,密码user01,角色user
        userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
        //创建用户admin01,密码admin01,角色admin
        userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());

        return userDetailsManager;
    }

JdbcUserDetailsManagerInMemoryUserDetailsManager在用法上没有太大区别,只是多了设置DataSource的环节。Spring Security 通过DataSource执行设定好的命令。例如,此处的createUser函数实际上就是执行了下面的SQL语句:

insert into users (username,password,enabled) values(?,?,?)

查看 JdbcUserDetailsManager 的源代码可以看到更多定义好的 SQL 语句,诸如deleteUserSqlupdateUserSql等,这些都是JdbcUserDetailsManager与数据库实际交互的形式。当然,JdbcUserDetailsManager 也允许我们在特殊情况下自定义这些 SQL 语句,如有必要,调用对应的setXxxSql方法即可。
在这里插入图片描述
现在重启服务,我们发现看看Spring Security在数据库中生成了下面这些数据:
users表:
在这里插入图片描述
authorities表:
在这里插入图片描述
重启服务后,使用user01用户和admin01用户都能够正常合理访问接口,与预期的行为一致。

到目前为止,一切都工作得很好,但是只要我们重启服务,应用就会报错。这是因为users表在创建语句时,username字段为主键,主键是唯一不重复的,但重启服务后会再次创建admin和user,导致数据库报错(在内存数据源上不会出现这种问题,因为重启服务后会清空username字段中的内容)。
所以如果需要在服务启动时便生成部分用户,那么建议先判断用户名是否存在。

    @Bean
    public UserDetailsService userDetailsService(DataSource dataSource){
        JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();
        userDetailsManager.setDataSource(dataSource);

        //创建用户user01,密码user01,角色user
        if (!userDetailsManager.userExists("user01")) { //判断user01是否存在
            userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
        }
        //创建用户admin01,密码admin01,角色admin
        if (!userDetailsManager.userExists("admin01")) {//判断admin01是否存在
            userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
        }
        return userDetailsManager;
    }

补充

WebSecurityConfigurer Adapter定义了三个configure:

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        this.disableLocalConfigureAuthenticationBldr = true;
    }
    public void configure(WebSecurity web) throws Exception {
    }

    protected void configure(HttpSecurity http) throws Exception {
        this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
        ((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
    }

我们只用到了一个参数,用来接收 HttpSecurity 对象的配置方法。另外两个参数也有各自的用途,其中,AuthenticationManagerBuilder的configure同样允许我们配置认证用户:

@EnableWebSecurity
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
                .antMatchers("/api/user/**").hasAnyRole("user")  //user 角色访问/api/user/开头的路由
                .antMatchers("/api/admin/**").hasAnyRole("admin") //admin 角色访问/api/admin/开头的路由
                .antMatchers("/api/public/**").permitAll()                 //允许所有可以访问/api/public/开头的路由
                .and()
            .formLogin();
    }

//    @Bean
//    public UserDetailsService userDetailsService(){
//        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
//
//        //创建用户user01,密码user01,角色user
//        userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
//        //创建用户admin01,密码admin01,角色admin
//        userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
//
//        return userDetailsManager;
//    }

//    @Bean
//    public UserDetailsService userDetailsService(DataSource dataSource){
//        JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager();
//        userDetailsManager.setDataSource(dataSource);
//
//        //创建用户user01,密码user01,角色user
//        if (!userDetailsManager.userExists("user01")) { //判断user01是否存在
//            userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("user01").password("user01").roles("user").build());
//        }
//        //创建用户admin01,密码admin01,角色admin
//        if (!userDetailsManager.userExists("admin01")) {//判断admin01是否存在
//            userDetailsManager.createUser(User.withDefaultPasswordEncoder().username("admin01").password("admin01").roles("admin").build());
//        }
//        return userDetailsManager;
//    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("user1")
                .password(new BCryptPasswordEncoder().encode("user01"))
                .roles("user")
                .and()
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin01")
                .password(new BCryptPasswordEncoder().encode("admin01"))
                .roles("admin");
    }
}

自定义数据库模型的认证于授权

InMemoryUserDetailsManagerJdbcUserDetailsManager两个类都是UserDetailsService的实现类,自定义数据库结构实际上也仅需实现一个自定义的UserDetailsService
UserDetailsService仅定义了一个loadUserByUsername方法,用于获取一个UserDetails对象。UserDetails对象包含了一系列在验证时会用到的信息,包括用户名、密码、权限以及其他信息,Spring Security 会根据这些信息判定验证是否成功。

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

也就是说,不管数据库结构如何变化,只要能构造一个UserDetails即可。

自定义实现UserDetail

1.编写实体User实现UserDetail

public class User implements UserDetails {
    private Long id;
    private String userName;
    private String password;
    private Boolean enable;
    private String roles;
    private List<GrantedAuthority> authentications;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getRoles() {
        return roles;
    }

    public void setRoles(String roles) {
        this.roles = roles;
    }

    public Boolean getEnable() {
        return enable;
    }

    public void setEnable(Boolean enable) {
        this.enable = enable;
    }

    public List<GrantedAuthority> getAuthentications() {
        return authentications;
    }

    public void setAuthentications(List<GrantedAuthority> authentications) {
        this.authentications = authentications;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.getAuthentications();
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.userName;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.enable;
    }
}

实现UserDetails定义的几个方法:

  • isAccountNonExpiredisAccountNonLockedisCredentialsNonExpired 暂且用不到,统一返回rue,否则Spring Security会认为账号异常。
  • isEnabled对应enable字段,将其代入即可。
  • getAuthorities方法本身对应的是roles字段,但由于结构不一致,所以此处新建一个,并在后续进行填充。

2.数据库持久层

这里使用JPA 实现实体关系型映射,建立实体与数据库的关系:

(1)需要引入spring-boot-stater-data-jpa依赖

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

(2)更改User为数据库映射实体类:

@Entity
@Table
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    private String userName;
    private String password;
    private Boolean enable;
    private String roles;
    
    @Transient
    private List<GrantedAuthority> authentications;
	....
	....

(3)新建UserRepository

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

自定义实现UserDetailsService

@Service
public class MyUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //(1)从数据库获取用户
        User user = userRepository.findByUserName(username);
        if (user==null)//用户不存在
            throw new RuntimeException("用户"+username+"不存在!");
        //(2)将数据库中的roles解析为UserDetail的权限集
        String roles = user.getRoles();
        List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(roles);
        user.setAuthentications(grantedAuthorities);
        return user;
    }
}

AuthorityUtils.commaSeparatedStringToAuthorityList(String list) 是spring security 提供的将逗号隔开的权限集字符串切割为权限对象列表,当然上面代码中我们也可以自己实现来代替:

    List<GrantedAuthority> getGrantedAuthorities(String roles){
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        String[] split = StringUtils.split(roles, ";");
        for (int i = 0;i<split.length;i++){
            if (!StringUtils.isEmpty(split[i])){
                SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(split[i]);
                grantedAuthorities.add(grantedAuthority);
            }
        }
        return grantedAuthorities;
    }

至此,我们就实现了Spring Security的自定义数据库结构认证工程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值