Spring Security登录账户自定义与数据持久化(5)

打印 上一主题 下一主题

主题 855|帖子 855|积分 2565

1.用户定义

  在前面的案例中,我们的登录用户是基于配置文件来配置的(本质是基于内存),但是在实际开发中,这种方式肯定是不可取的,在实际项目中,用户信息肯定要存入数据库之中。
  Spring Security支持多种用户定义方式,接下来我们就逐个来看一下这些定义方式。通过前面的介绍(参见3小节),大家对于UserDetailsService以及它的子类都有了一定的了解, 自定义用户其实就是使用UserDetailsService的不同实现类来提供用户数据,同时将配置好的 UserDetailsService 配置给 AuthenticationManagerBuilder,系统再将 UserDetailsSeivice 提供给 AuthenticationProvider 使用,
  1.1 基于内存

  前面案例中用户的定义本质上还是基于内存,只是我们没有将InMemoryUserDetailsManager类明确抽出来自定义,现在我们通过自定义InMemoryUserDetailsManager来看一下基于内存的用户是如何自定义的。
    重写 WebSecurityConfigurerAdapter 类的 configure(AuthenticationManagerBuilder)方法,内容如下:
  1. @Override
  2. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  3.     InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
  4.     manager.createUser(User.withUsername("buretuzi").password("{noop}123456").roles("admin").build());
  5.     manager.createUser(User.withUsername("sang").password("{noop}123").roles("user").build());
  6.     auth.userDetailsService(manager);
  7. }
复制代码
  首先构造了一个InMemoryUserDetailsManager实例,调用该实例的createUser方法来创建用户对象,我们在这里分别设置了用户名、密码以及用户角色。需要注意的是,用户密码加了 一个{noop}前缀,表示密码不加密,明文存储(关于密码加密问题,会在后面的章节中专门介绍)。
  配置完成后,启动项目,此时就可以使用这里配置的两个用户登录了。
  InMemoryUserDetailsManager 的实现原理很简单,它间接实现了 UserDetailsService 接口并重写了它里边的 loadUserByUsername方法,同时它里边维护了 一个HashMap变量,Map的 key 就是用户名,value则是用户对象,createUser就是往这个Map中存储数据,loadUserByUsername方法则是从该Map中读取数据,这里的源码比较简单,就不贴出来了,读者可以自行查看。
  1.2  基于JdbcUserDetailsManager

  JdbcUserDetailsManager支持将用户数据持久化到数据库,同时它封装了一系列操作用户的方法,例如用户的添加、更新、查找等。
   Spring Security 中为 JdbcUserDetailsManager 提供了数据库脚本,位置在 org/springframework/security/core/userdetails/jdbc/users.ddl 内容如下:(注意将varchar_ignorecase改为varchar)
  1. create table users(username varchar_ignorecase(50) not null primary key,
  2.                    password varchar_ignorecase(500) not null,
  3.                    enabled boolean not null);
  4. create table authorities (username varchar_ignorecase(50) not null,
  5.                           authority varchar_ignorecase(50) not null,
  6.                           constraint fk_authorities_users foreign key(username) references users(username));
  7. create unique index ix_auth_username on authorities (username,authority);
复制代码
  可以看到这里一共创建了两张表,users表就是存放用户信息的表,authorities则是存放用户角色的表。但是大家注意SQL的数据类型中有一个varchar_ignorecase,这个其实是针对 HSQLDB 的数据类型,我们这里使用的是MySQL数据库,所以这里手动将varchar_ignorecase 类型修改为varchar类型,然后去数据库中执行修改后的脚本。
  另一方面,由于要将数据存入数据库中,所以我们的项目也要提供数据库支持, JdbcUserDetailsManager底层实际上是使用JdbcTemplate来完成的,所以这里主要添加两个依赖:
  1. <dependency>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-starter-jdbc</artifactId>
  4. </dependency>
  5. <dependency>
  6.     <groupId>mysql</groupId>
  7.     <artifactId>mysql-connector-java</artifactId>
  8.     <scope>runtime</scope>
  9. </dependency>
复制代码
  然后在resources/application.yml中配置数据库连接信息:
  1. spring:
  2.   datasource:
  3.     druid:
  4.       driver-class-name: com.mysql.cj.jdbc.Driver
  5.       url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
  6.       username: root
  7.       password: 123456
复制代码
  配置完成后,我们重写WebSecurityConfigurerAdapter类的 configure(AuthenticationManagerBuilder auth)方法,内容如下(注意版本,不得低于以下版本):
  1. <dependency>
  2.     <groupId>org.springframework.security</groupId>
  3.     <artifactId>spring-security-web</artifactId>
  4.     <version>5.3.6.RELEASE</version>
  5. </dependency>
  6. <dependency>
  7.     <groupId>org.springframework.security</groupId>
  8.     <artifactId>spring-security-core</artifactId>
  9.     <version>5.3.6.RELEASE</version>
  10. </dependency>
复制代码
  1. package com.intehel.demo.config;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  5. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  6. import org.springframework.security.core.userdetails.User;
  7. import org.springframework.security.provisioning.JdbcUserDetailsManager;
  8. import javax.sql.DataSource;
  9. @Configuration
  10. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  11.     @Autowired
  12.     DataSource dataSource;
  13.     @Override
  14.     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  15.         JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
  16.         if (!manager.userExists("buretuzi")){
  17.             manager.createUser(User.withUsername("buretuzi").password("{noop}123456").roles("admin").build());
  18.         }
  19.         if (!manager.userExists("song")){
  20.             manager.createUser(User.withUsername("sang").password("{noop}123").roles("user").build());
  21.         }
  22.         auth.userDetailsService(manager);
  23.     }
  24. }
复制代码

  • 当引入spring-boot-starter-jdbc并配置了数据库连接信息后,一个DataSource实例就有了,这里首先引入DataSource实例。
  • 在 configure 方法中,创建一个 JdbcUserDetailsManager 实例,在创建时传入 DataSource 实例。通过userExists方法可以判断一个用户是否存在,该方法本质上就是去数据库中査询对应的用户;如果用户不存在,则通过createUser方法可以创建一个用户,该方法本质上就是向数据库中添加一个用户。
  • 最后将manager实例设置到auth对象中。
  配置完成后,重启项目,如果项目启动成功,数据库中就会自动添加进来两条数据,如图2-22、图2-23所示。
  
图 2-22

图 2-23
  此时,我们就可以使用buretuzi/123456,sang/123进行登录测试了。
  在 JdbcUserDetailsManager 的继承体系中,首先是 JdbcDaoImpl 实现了 UserDetailsService 接口,并实现了基本的loadUserByUsername方法,JdbcUserDetailsManager则继承自 JdbcDaoImpl,同时完善了数据库操作,又封装了用户的增删改査方法,这里,我们以 loadUserByUsername为例,看一下源码,其余的增删改操作相对来说都比较容易,这里就不再赘述了。
  JdbcDaoImpl#loadUserByUsername:
  1. public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService, MessageSourceAware {
  2.     public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled from users where username = ?";
  3.     public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username,authority from authorities where username = ?";
  4.     public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY = "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id";
  5.     protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
  6.     private String authoritiesByUsernameQuery = "select username,authority from authorities where username = ?";
  7.     private String groupAuthoritiesByUsernameQuery = "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id";
  8.     private String usersByUsernameQuery = "select username,password,enabled from users where username = ?";
  9.     private String rolePrefix = "";
  10.     private boolean usernameBasedPrimaryKey = true;
  11.     private boolean enableAuthorities = true;
  12.     private boolean enableGroups;
  13.    
  14.     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  15.         List<UserDetails> users = this.loadUsersByUsername(username);
  16.         if (users.size() == 0) {
  17.             this.logger.debug("Query returned no results for user '" + username + "'");
  18.             throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.notFound", new Object[]{username}, "Username {0} not found"));
  19.         } else {
  20.             UserDetails user = (UserDetails)users.get(0);
  21.             Set<GrantedAuthority> dbAuthsSet = new HashSet();
  22.             if (this.enableAuthorities) {
  23.                 dbAuthsSet.addAll(this.loadUserAuthorities(user.getUsername()));
  24.             }
  25.             if (this.enableGroups) {
  26.                 dbAuthsSet.addAll(this.loadGroupAuthorities(user.getUsername()));
  27.             }
  28.             List<GrantedAuthority> dbAuths = new ArrayList(dbAuthsSet);
  29.             this.addCustomAuthorities(user.getUsername(), dbAuths);
  30.             if (dbAuths.size() == 0) {
  31.                 this.logger.debug("User '" + username + "' has no authorities and will be treated as 'not found'");
  32.                 throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.noAuthority", new Object[]{username}, "User {0} has no GrantedAuthority"));
  33.             } else {
  34.                 return this.createUserDetails(username, user, dbAuths);
  35.             }
  36.         }
  37.     }
  38.     protected List<UserDetails> loadUsersByUsername(String username) {
  39.         return this.getJdbcTemplate().query(this.usersByUsernameQuery, new String[]{username}, (rs, rowNum) -> {
  40.             String username1 = rs.getString(1);
  41.             String password = rs.getString(2);
  42.             boolean enabled = rs.getBoolean(3);
  43.             return new User(username1, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES);
  44.         });
  45.     }
  46. }
复制代码

  • 首先根据用户名,调用loadUserByUsername方法去数据库中查询用户,查询出来的是一个List集合,集合中如果没有数据,说明用户不存在,则直接抛出异常,
  • 如果集合中存在数据,则将集合中的第一条数据拿出来,然后再去查询用户角色, 最后根据这些信息创建一个新的UserDetails出来。
  • 需要注意的是,这里还引入了分组的概念,不过考虑到JdbcUserDetailsManager并非我们实际项目中的主流方案,因此这里不做过多介绍。
  这就是使用JdbcUserDetailsManager做数据持久化。这种方式看起来简单,都不用开发者自己写SQL,但是局限性比较大,无法灵活地定义用户表、角色表等,而在实际开发中,我们还是希望能够灵活地掌控数据表结构,因此JdbcUserDetailsManager使用场景非常有限。
  1.3 基于 MyBatis

  使用MyBatis做数据持久化是目前大多数企业应用釆取的方案,Spring Security中结合 MyBatis可以灵活地定制用户表以及角色表,我们对此进行详细介绍。
  首先需要设计三张表,分别是用户表、角色表以及用户角色关联表,三张表的关系如图 2-24所示。
  
图 2-24
  用户和角色是多对多的关系,我们使用user_role来将两者关联起来。 数据库脚本如下:
  1. CREATE TABLE `role`(
  2.         `id` INT(11) NOT NULL AUTO_INCREMENT,
  3.         `name` VARCHAR(32) DEFAULT NULL,
  4.         `nameZh` VARCHAR(32) DEFAULT NULL,
  5.         PRIMARY KEY(`id`)
  6. )ENGINE=INNODB DEFAULT CHARSET=utf8
  7. CREATE TABLE `user` (
  8.         `id` INT(11) NOT NULL AUTO_INCREMENT,
  9.         `username` VARCHAR(32) DEFAULT NULL,
  10.         `password` VARCHAR(255) DEFAULT NULL,
  11.         `enabled` TINYINT(1) DEFAULT NULL,
  12.         `accountNonExpired` TINYINT(1) DEFAULT NULL,
  13.         `accountNonLocked` TINYINT(1) DEFAULT NULL,
  14.         `credentialsNonExpired` TINYINT(1) DEFAULT NULL,
  15.         PRIMARY KEY(`id`)
  16. )ENGINE=INNODB DEFAULT CHARSET=utf8
  17. CREATE TABLE `user_role`(
  18.         `id` INT(11) NOT NULL AUTO_INCREMENT,
  19.         `uid` INT(11) DEFAULT NULL,
  20.         `rid` INT(11) DEFAULT NULL,
  21.         PRIMARY KEY(`id`),
  22.         KEY `uid` (`uid`),
  23.         KEY `rid` (`rid`)
  24. )ENGINE=INNODB DEFAULT CHARSET=utf8
复制代码
  对于角色表,三个字段从上往下含义分别为角色id、角色英文名称以及角色中文名称, 对于用户表,七个字段从上往下含义依次为:用户id、用户名、用户密码、账户是否可用、账户是否没有过期、账户是否没有锁定以及凭证(密码)是否没有过期。
  数据库创建完成后,可以向数据库中添加几条模拟数据,代码如下:
  1. INSERT INTO `role` (`id`,`name`,`nameZh`)
  2. VALUES
  3.         (1,'ROLE_dba','数据库管理员'),
  4.         (2,'ROLE_admin','系统管理员'),
  5.         (3,'ROLE_user','用户');
  6. INSERT INTO `user` (`id`,`username`,`password`,`enabled`,`accountNonExpired`,`accountNonLocked`,`credentialsNonExpired`)
  7. VALUES
  8.         (1,'root','{noop}123',1,1,1,1),
  9.         (2,'admin','{noop}123',1,1,1,1),
  10.         (3,'sang','{noop}123',1,1,1,1);
  11. INSERT INTO `user_role` (`id`,`uid`,`rid`)
  12. VALUES
  13.         (1,1,1),
  14.         (2,1,2),
  15.         (3,2,2),
  16.         (4,3,3);
复制代码
  这样,数据库的准备工作就算完成了。
  在Spring Security项目中,我们需要引入MyBatis和MySQL依赖,代码如下:
  1. <dependency>
  2.     <groupId>org.mybatis.spring.boot</groupId>
  3.     <artifactId>mybatis-spring-boot-starter</artifactId>
  4.     <version>2.2.2</version>
  5. </dependency>
  6. <dependency>
  7.     <groupId>mysql</groupId>
  8.     <artifactId>mysql-connector-java</artifactId>
  9.     <scope>runtime</scope>
  10. </dependency>
复制代码
  同时在resources/application.yml中配置数据库基本连接信息:
  1. spring:
  2.   datasource:
  3.     druid:
  4.       driver-class-name: com.mysql.cj.jdbc.Driver
  5.       url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
  6.       username: root
  7.       password: 123456
复制代码
  接下来创建用户类和角色类:
  1. package com.intehel.demo.domain;
  2. import org.springframework.security.core.GrantedAuthority;
  3. import org.springframework.security.core.authority.SimpleGrantedAuthority;
  4. import org.springframework.security.core.userdetails.UserDetails;
  5. import java.util.ArrayList;
  6. import java.util.Collection;
  7. import java.util.List;
  8. import lombok.Data;
  9. @Data
  10. public class User implements UserDetails {
  11.     private Integer id;
  12.     private String username;
  13.     private String password;
  14.     private Boolean enabled;
  15.     private Boolean accountNonExpired;
  16.     private Boolean accountNonLocked;
  17.     private Boolean credentialsNonExpired;
  18.     private List<Role> roles = new ArrayList<Role>();
  19.     @Override
  20.     public Collection<? extends GrantedAuthority> getAuthorities() {
  21.         List<SimpleGrantedAuthority> authorities = new ArrayList<>();
  22.         for (Role role : roles) {
  23.             authorities.add(new SimpleGrantedAuthority(role.getName()));
  24.         }
  25.         return authorities;
  26.     }
  27.     @Override
  28.     public boolean isAccountNonExpired() {
  29.         return accountNonExpired;
  30.     }
  31.     @Override
  32.     public boolean isAccountNonLocked() {
  33.         return accountNonLocked;
  34.     }
  35.     @Override
  36.     public boolean isCredentialsNonExpired() {
  37.         return credentialsNonExpired;
  38.     }
  39.     @Override
  40.     public boolean isEnabled() {
  41.         return enabled;
  42.     }
  43. }
复制代码
  将mylogin.html放在 resources/templates/ 下,mylogin.html如下
查看代码
  1. package com.intehel.demo.domain;
  2. import lombok.Data;
  3. @Data
  4. public class Role {
  5.     private Integer id;
  6.     private String name;
  7.     private String nameZh;
  8. }
复制代码
  为了方便,我们将UserMapper.xml文件放在resources/mapper下,UsetMapper接口放在mapper包下。为了防止 Maven打包时自动忽略了 XML文件,还需要在application.yml中添加mapper-locations配置:
查看代码
  1. # 应用名称spring:
  2.   datasource:
  3.     druid:
  4.       driver-class-name: com.mysql.cj.jdbc.Driver
  5.       url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
  6.       username: root
  7.       password: 123456  security:    user:      name: buretuzi      password: 123456  application:    name: demo  thymeleaf:    mode: HTML    encoding: UTF-8    servlet:      content-type: text/html    cache: false    prefix: classpath:/templates/        # 应用服务 WEB 访问端口server:  port: 8080mybatis:  # spring boot集成mybatis的方式打印sql  mapper-locations: classpath:/mapper/*.xml  configuration:    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
复制代码
  最后一步,就是在 SecurityConfig 中注入 UserDetailsService:
查看代码
  1. package com.intehel.demo.service;
  2. import com.intehel.demo.domain.User;
  3. import com.intehel.demo.mapper.UserMapper;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.security.core.userdetails.UserDetails;
  6. import org.springframework.security.core.userdetails.UserDetailsService;
  7. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  8. public class MyUserDetailsService implements UserDetailsService {
  9.     @Autowired
  10.     UserMapper userMapper;
  11.     @Override
  12.     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  13.         User user = userMapper.loadUserByUsername(username);
  14.         if (user == null) {
  15.             throw new UsernameNotFoundException("用户不存在");
  16.         }
  17.         user.setRoles(userMapper.getRolesByUid(user.getId()));
  18.         return user;
  19.     }
  20. }
复制代码
   配置UserDetailsService的方式和前面配置JdbcUserDetailsManager的方式基本一致,只不过配置对象变成了 myUserDetailsService而己。至此,整个配置工作就完成了。
  接下来启动项目,利用数据库中添加的模拟用户进行登录测试,就可以成功登录了,测试方式和前面章节一致,这里不再赘述。
  1.4  基于 Spring Data JPA

    考虑到在Spring Boot技术栈中也有不少人使用Spring Data JPA,因此这里针对Spring Security+Spring Data JPA也做一个简单介绍,具体思路和基于MyBatis的整合类似。
   首先引入Spring Data JPA的依赖和MySQL依赖:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  3.         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  4. <mapper namespace="com.intehel.demo.mapper.UserMapper">
  5.     <select id="loadUserByUsername" resultType="com.intehel.demo.domain.User">
  6.         select * from `user` where username = #{username}
  7.     </select>
  8.     <select id="loadUserByUsername" resultType="com.intehel.demo.domain.Role">
  9.         select r.* from role r,user_role ur where r.`id`=ur.`rid`
  10.     </select>
  11. </mapper>
复制代码
  然后在resources/application.yml中配置数据库和JPA,代码如下:
  1. spring:
  2.   datasource:
  3.     druid:
  4.       driver-class-name: com.mysql.cj.jdbc.Driver
  5.       url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
  6.       username: root
  7.       password: 123456  jpa:    database: mysql    database-platform: mysql    hibernate:      ddl-auto: update    show-sql: true    properties:      hibernate:        dialect: org.hibernate.dialect.Mysql8Dialect
复制代码
  据库的配置还是和以前一样,JPA的配置则主要配置了数据库平台,数据表更新方式、 是否打印SQL以及对应的数据库方言。
  使用Spring Data JPA的好处是我们不用提前准备SQL脚本,所以接下来配置两个数据库实体类即可:
[code]package com.intehel.demo.domain;import lombok.Data;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import javax.persistence.*;import java.util.ArrayList;import java.util.Collection;import java.util.List;@Entity@Datapublic class User implements UserDetails {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    private long id;    private String username;    private String password;    private Boolean enabled;    private Boolean accountNonExpired;    private Boolean accountNonLocked;    private Boolean credentialsNonExpired;    @ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.PERSIST)    private List roles;    @Override    public Collection
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

罪恶克星

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表