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

news2024/11/17 3:53:29

1、用户自定义

在前面的案例中,我们的登录用户是基于配置文件来配置的(本质是基于内存),但是在实际开发中,这种方式肯定是不可取的,在实际项目中,用户信息肯定要存入数据库之中。

Spring Security支持多种用户定义方式,接下来我们就逐个来看一下这些定义方式。通过前面的介绍(参见3小节),大家对于UserDetailsService以及它的子类都有了一定的了解, 自定义用户其实就是使用UserDetailsService的不同实现类来提供用户数据,同时将配置好的 UserDetailsService 配置给 AuthenticationManagerBuilder,系统再将 UserDetailsSeivice 提供给 AuthenticationProvider 使用。

1.1、基于内存

前面案例中用户的定义本质上还是基于内存,只是我们没有将InMemoryUserDetailsManager类明确抽出来自定义,现在我们通过自定义InMemoryUserDetailsManager来看一下基于内存的用户是如何自定义的。

重写 WebSecurityConfigurerAdapter 类的 configure(AuthenticationManagerBuilder)方法,内容如下:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("buretuzi").password("{noop}123456").roles("admin").build());
    manager.createUser(User.withUsername("sang").password("{noop}123").roles("user").build());
    auth.userDetailsService(manager);
}

首先构造了一个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)

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);

可以看到这里一共创建了两张表,users表就是存放用户信息的表,authorities则是存放用户角色的表。但是大家注意SQL的数据类型中有一个varchar_ignorecase,这个其实是针对 HSQLDB 的数据类型,我们这里使用的是MySQL数据库,所以这里手动将varchar_ignorecase 类型修改为varchar类型,然后去数据库中执行修改后的脚本。

另一方面,由于要将数据存入数据库中,所以我们的项目也要提供数据库支持, JdbcUserDetailsManager底层实际上是使用JdbcTemplate来完成的,所以这里主要添加两个依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

然后在resources/application.yml中配置数据库连接信息:

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
      username: root
      password: 123456

配置完成后,我们重写WebSecurityConfigurerAdapter类的 configure(AuthenticationManagerBuilder auth)方法,内容如下(注意版本,不得低于以下版本):

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.3.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>5.3.6.RELEASE</version>
</dependency>
package com.intehel.demo.config;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import javax.sql.DataSource;
 
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    DataSource dataSource;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
        if (!manager.userExists("buretuzi")){
            manager.createUser(User.withUsername("buretuzi").password("{noop}123456").roles("admin").build());
        }
        if (!manager.userExists("song")){
            manager.createUser(User.withUsername("sang").password("{noop}123").roles("user").build());
        }
        auth.userDetailsService(manager);
    }
}
  • 当引入spring-boot-starter-jdbc并配置了数据库连接信息后,一个DataSource实例就有了,这里首先引入DataSource实例。
  • 在 configure 方法中,创建一个 JdbcUserDetailsManager 实例,在创建时传入 DataSource 实例。通过userExists方法可以判断一个用户是否存在,该方法本质上就是去数据库中査询对应的用户;如果用户不存在,则通过createUser方法可以创建一个用户,该方法本质上就是向数据库中添加一个用户。
  • 在 configure 方法中,创建一个 JdbcUserDetailsManager 实例,在创建时传入 DataSource 实例。通过userExists方法可以判断一个用户是否存在,该方法本质上就是去数据库中査询对应的用户;如果用户不存在,则通过createUser方法可以创建一个用户,该方法本质上就是向数据库中添加一个用户。

配置完成后,重启项目,如果项目启动成功,数据库中就会自动添加进来两条数据,如图2-22、图2-23所示。
图 2-22

图 2-23
此时,我们就可以使用buretuzi/123456,sang/123进行登录测试了。

在 JdbcUserDetailsManager 的继承体系中,首先是 JdbcDaoImpl 实现了 UserDetailsService 接口,并实现了基本的loadUserByUsername方法,JdbcUserDetailsManager则继承自 JdbcDaoImpl,同时完善了数据库操作,又封装了用户的增删改査方法,这里,我们以 loadUserByUsername为例,看一下源码,其余的增删改操作相对来说都比较容易,这里就不再赘述了。

JdbcDaoImpl#loadUserByUsername:

public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService, MessageSourceAware {
    public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled from users where username = ?";
    public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username,authority from authorities where username = ?";
    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";
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private String authoritiesByUsernameQuery = "select username,authority from authorities where username = ?";
    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";
    private String usersByUsernameQuery = "select username,password,enabled from users where username = ?";
    private String rolePrefix = "";
    private boolean usernameBasedPrimaryKey = true;
    private boolean enableAuthorities = true;
    private boolean enableGroups;
    
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<UserDetails> users = this.loadUsersByUsername(username);
        if (users.size() == 0) {
            this.logger.debug("Query returned no results for user '" + username + "'");
            throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.notFound", new Object[]{username}, "Username {0} not found"));
        } else {
            UserDetails user = (UserDetails)users.get(0);
            Set<GrantedAuthority> dbAuthsSet = new HashSet();
            if (this.enableAuthorities) {
                dbAuthsSet.addAll(this.loadUserAuthorities(user.getUsername()));
            }
 
            if (this.enableGroups) {
                dbAuthsSet.addAll(this.loadGroupAuthorities(user.getUsername()));
            }
 
            List<GrantedAuthority> dbAuths = new ArrayList(dbAuthsSet);
            this.addCustomAuthorities(user.getUsername(), dbAuths);
            if (dbAuths.size() == 0) {
                this.logger.debug("User '" + username + "' has no authorities and will be treated as 'not found'");
                throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.noAuthority", new Object[]{username}, "User {0} has no GrantedAuthority"));
            } else {
                return this.createUserDetails(username, user, dbAuths);
            }
        }
    }
 
    protected List<UserDetails> loadUsersByUsername(String username) {
        return this.getJdbcTemplate().query(this.usersByUsernameQuery, new String[]{username}, (rs, rowNum) -> {
            String username1 = rs.getString(1);
            String password = rs.getString(2);
            boolean enabled = rs.getBoolean(3);
            return new User(username1, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES);
        });
    }
}
  • 首先根据用户名,调用loadUserByUsername方法去数据库中查询用户,查询出来的是一个List集合,集合中如果没有数据,说明用户不存在,则直接抛出异常。
  • 如果集合中存在数据,则将集合中的第一条数据拿出来,然后再去查询用户角色, 最后根据这些信息创建一个新的UserDetails出来。
  • 需要注意的是,这里还引入了分组的概念,不过考虑到JdbcUserDetailsManager并非我们实际项目中的主流方案,因此这里不做过多介绍。

这就是使用JdbcUserDetailsManager做数据持久化。这种方式看起来简单,都不用开发者自己写SQL,但是局限性比较大,无法灵活地定义用户表、角色表等,而在实际开发中,我们还是希望能够灵活地掌控数据表结构,因此JdbcUserDetailsManager使用场景非常有限。

1.3、基于 MyBatis

使用MyBatis做数据持久化是目前大多数企业应用釆取的方案,Spring Security中结合 MyBatis可以灵活地定制用户表以及角色表,我们对此进行详细介绍。

首先需要设计三张表,分别是用户表、角色表以及用户角色关联表,三张表的关系如图 2-24所示。
图 2-24

用户和角色是多对多的关系,我们使用user_role来将两者关联起来。 数据库脚本如下:

CREATE TABLE `role`(
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(32) DEFAULT NULL,
	`nameZh` VARCHAR(32) DEFAULT NULL,
	PRIMARY KEY(`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8
 
CREATE TABLE `user` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`username` VARCHAR(32) DEFAULT NULL,
	`password` VARCHAR(255) DEFAULT NULL,
	`enabled` TINYINT(1) DEFAULT NULL,
	`accountNonExpired` TINYINT(1) DEFAULT NULL,
	`accountNonLocked` TINYINT(1) DEFAULT NULL,
	`credentialsNonExpired` TINYINT(1) DEFAULT NULL,
	PRIMARY KEY(`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8
 
CREATE TABLE `user_role`(
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`uid` INT(11) DEFAULT NULL,
	`rid` INT(11) DEFAULT NULL,
	PRIMARY KEY(`id`),
	KEY `uid` (`uid`),
	KEY `rid` (`rid`)
)ENGINE=INNODB DEFAULT CHARSET=utf8

对于角色表,三个字段从上往下含义分别为角色id、角色英文名称以及角色中文名称, 对于用户表,七个字段从上往下含义依次为:用户id、用户名、用户密码、账户是否可用、账户是否没有过期、账户是否没有锁定以及凭证(密码)是否没有过期。

数据库创建完成后,可以向数据库中添加几条模拟数据,代码如下:

INSERT INTO `role` (`id`,`name`,`nameZh`)
VALUES
	(1,'ROLE_dba','数据库管理员'),
	(2,'ROLE_admin','系统管理员'),
	(3,'ROLE_user','用户');
 
INSERT INTO `user` (`id`,`username`,`password`,`enabled`,`accountNonExpired`,`accountNonLocked`,`credentialsNonExpired`)
VALUES
	(1,'root','{noop}123',1,1,1,1),
	(2,'admin','{noop}123',1,1,1,1),
	(3,'sang','{noop}123',1,1,1,1);
 
INSERT INTO `user_role` (`id`,`uid`,`rid`)
VALUES
	(1,1,1),
	(2,1,2),
	(3,2,2),
	(4,3,3);

这样,数据库的准备工作就算完成了。

在Spring Security项目中,我们需要引入MyBatis和MySQL依赖,代码如下:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

同时在resources/application.yml中配置数据库基本连接信息:

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
      username: root
      password: 123456

接下来创建用户类和角色类:

package com.intehel.demo.domain;
 
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import lombok.Data;
 
@Data
public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean accountNonExpired;
    private Boolean accountNonLocked;
    private Boolean credentialsNonExpired;
    private List<Role> roles = new ArrayList<Role>();
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }
    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }
    @Override
    public boolean isEnabled() {
        return enabled;
    }
}
package com.intehel.demo.domain;
import lombok.Data;
@Data
public class Role {
    private Integer id;
    private String name;
    private String nameZh;
}

自定义用户类需要实现UserDetails接口,并实现接口中的方法,这些方法的含义我们在 3小节中已经介绍过了,这里不再赘述。其中roles属性用来保存用户所具备的角色信息, 由于系统获取用户角色调用的方法是getAuthorities,所以我们在getAuthorities方法中,将roles 中的角色转为系统可识别的对象并返回。

package com.intehel.demo.mapper;
 
import com.intehel.demo.domain.Role;
import com.intehel.demo.domain.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
 
@Mapper
public interface UserMapper{
    List<Role> getRolesByUid(Integer id);
    User loadUserByUsername(String username);
}
package com.intehel.demo.service;
 
import com.intehel.demo.domain.User;
import com.intehel.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
 
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        user.setRoles(userMapper.getRolesByUid(user.getId()));
        return user;
    }
}

自定义 MyUserDetailsService实现UserDetailsSeivice接口,并实现该接口中的方法。 loadUserByUsername方法经过前面章节的讲解,相信大家已经很熟悉了,该方法就是根据用户名去数据库中加载用户,如果从数据库中没有査到用户,则抛出UsemameNotFoundException 异常;如果査询到用户了,则给用户设置roles属性。

UserMapper中定义两个方法用于支持MyUserDetailsService中的査询操作。

最后,在UserMapper.xml中定义查询SQL,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.intehel.demo.mapper.UserMapper">
    <select id="loadUserByUsername" resultType="com.intehel.demo.domain.User">
        select * from `user` where username = #{username}
    </select>
 
    <select id="loadUserByUsername" resultType="com.intehel.demo.domain.Role">
        select r.* from role r,user_role ur where r.`id`=ur.`rid`
    </select>
</mapper>

将mylogin.html放在 resources/templates/ 下,mylogin.html如下:

 <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<style>
    #login .container #login-row #login-column #login-box {
        border: 1px solid #9c9c9c;
        background-color: #EAEAEA;
    }
</style>
<body>
<div id="login">
    <div class="container">
        <div id="login-row" class="row justify-content-center align-items-center">
            <div id="login-column" class="col-md-6">
                <div id="login-box" class="col-md-12">
                    <form id="login-form" class="form" action="/doLogin" method="post">
                        <h3 class="text-center text-info">登录</h3>
                        <!--/*@thymesVar id="SPRING_SECURITY_LAST_EXCEPTION" type="com"*/-->
                        <div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
                        <div class="form-group">
                            <label for="username" class="text-info">用户名:</label><br>
                            <input type="text" name="uname" id="username" class="form-control">
                        </div>
                        <div class="form-group">
                            <label for="password" class="text-info">密码:</label><br>
                            <input type="text" name="passwd" id="password" class="form-control">
                        </div>
                        <div class="form-group">
                            <input type="submit" name="submit" class="btn btn-info btn-md" value="登录">
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

为了方便,我们将UserMapper.xml文件放在resources/mapper下,UsetMapper接口放在mapper包下。为了防止 Maven打包时自动忽略了 XML文件,还需要在application.yml中添加mapper-locations配置:

# 应用名称
spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
      username: root
      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: 8080
mybatis:
  # spring boot集成mybatis的方式打印sql
  mapper-locations: classpath:/mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

最后一步,就是在 SecurityConfig 中注入 UserDetailsService:

 package com.intehel.demo.config;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.intehel.demo.handler.MyAuthenticationFailureHandler;
import com.intehel.demo.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
 
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    MyUserDetailsService myUserDetailsService;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(new MyAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),
                        new AntPathRequestMatcher("/logout2","POST")))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .defaultLogoutSuccessHandlerFor((req,resp,auth)->{
                    resp.setContentType("application/json;charset=UTF-8");
                    Map<String,Object> result = new HashMap<String,Object>();
                    result.put("status",200);
                    result.put("msg","使用logout1注销成功!");
                    ObjectMapper om = new ObjectMapper();
                    String s = om.writeValueAsString(result);
                    resp.getWriter().write(s);
                },new AntPathRequestMatcher("/logout1","GET"))
                .defaultLogoutSuccessHandlerFor((req,resp,auth)->{
                    resp.setContentType("application/json;charset=UTF-8");
                    Map<String,Object> result = new HashMap<String,Object>();
                    result.put("status",200);
                    result.put("msg","使用logout2注销成功!");
                    ObjectMapper om = new ObjectMapper();
                    String s = om.writeValueAsString(result);
                    resp.getWriter().write(s);
                },new AntPathRequestMatcher("/logout1","GET"))
                .and()
                .csrf().disable();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
    }
}

配置UserDetailsService的方式和前面配置JdbcUserDetailsManager的方式基本一致,只不过配置对象变成了 myUserDetailsService而己。至此,整个配置工作就完成了。

接下来启动项目,利用数据库中添加的模拟用户进行登录测试,就可以成功登录了,测试方式和前面章节一致,这里不再赘述。

1.4、基于 Spring Data JPA

考虑到在Spring Boot技术栈中也有不少人使用Spring Data JPA,因此这里针对Spring Security+Spring Data JPA也做一个简单介绍,具体思路和基于MyBatis的整合类似。

首先引入Spring Data JPA的依赖和MySQL依赖:

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

然后在resources/application.yml中配置数据库和JPA,代码如下:

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
      username: root
      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脚本,所以接下来配置两个数据库实体类即可:

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
@Data
public 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<Role> roles;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
    @Override
    public String getPassword() {
        return password;
    }
    @Override
    public String getUsername() {
        return username;
    }
    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }
    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }
    @Override
    public boolean isEnabled() {
        return enabled;
    }
}
package com.intehel.demo.domain;
 
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
 
@Data
@Entity(name = "role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private String nameZh;
}

这两个实体类和前面MyBatis中实体类的配置类似,需要注意的是roles属性上多了一个 多对多配置。

接下来配置UserDetailsService,并提供数据查询方法:

package com.intehel.demo.dao;
 
import com.intehel.demo.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
 
public interface UserDao extends JpaRepository<User,Integer> {
    User findUserByUsername(String username);
}
package com.intehel.demo.Service;
 
import com.intehel.demo.dao.UserDao;
import com.intehel.demo.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
 
@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    UserDao userDao;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.findUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return user;
    }
}

MyUserDetailsService的定义也和前面的类似,不同之处在于数据查询方法的变化。定义 UserDao 继承自 JpaRepository,并定义一个 findUserByUsername 方法,剩下的事情 Spring Data JPA框架会帮我们完成。

最后,再在 SecurityConfig 中配置 MyUserDetailsService配置方式和 MyBatis 一模一样, 这里就不再把代码贴岀来了。使用了 Spring Data JPA之后,当项目启动时,会自动在数据库中创建相关的表,而不用我们自己去写脚本,这也是使用Spring Data JPA的方便之处。

为了测试方便,我们可以在单元测试中执行如下代码,向数据库中添加测试数据:

package com.intehel.demo;
 
import com.intehel.demo.dao.UserDao;
import com.intehel.demo.domain.Role;
import com.intehel.demo.domain.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.List;
 
@SpringBootTest
class DemoApplicationTests {
    @Autowired
    UserDao userDao;
    @Test
    void contextLoads() {
        User user1 = new User();
        user1.setUsername("buretuzi");
        user1.setPassword("{noop}123");
        user1.setAccountNonExpired(true);
        user1.setAccountNonLocked(true);
        user1.setCredentialsNonExpired(true);
        user1.setEnabled(true);
        List<Role> roles = new ArrayList<>();
        Role r1 = new Role();
        r1.setName("ROLE_admin");
        r1.setNameZh("管理员");
        roles.add(r1);
        user1.setRoles(roles);
        userDao.save(user1);
    }
 
}

测试数据添加成功之后,接下来启动项目,使用测试数据进行登录测试,具体测试过程就不再赘述了。

至此,四种不同的用户定义方式就介绍完了。这四种方式,异曲同工,只是数据存储的方式不一样而已,其他的执行流程都是一样的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1115950.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python如何实现多线程编程

目录 线程的创建 线程的管理 线程的同步 线程池 线程同步和锁 总结 Python是一种广泛使用的编程语言&#xff0c;它具有丰富的库和工具&#xff0c;可以用来实现多线程编程。多线程编程是一种并行计算技术&#xff0c;它通过将任务划分为多个独立的任务并利用多个线程同时…

unity脚本_碰撞检测函数 c#

在项目创建一个脚本文件包新建脚本Cor 将以下代码复制 using UnityEngine; public class Cor : MonoBehaviour{ #region 碰撞检测函数 #endregion //至少一个刚体和两个碰撞器让两个游戏物体产生碰撞 //物理材质Phy Material让两个游戏物体之间表现不同效果 //…

英语——分享篇——每日200词——2201-2400

2201——retreat——[rɪtrɪ:t]——vi.退却&#xff0c;后退——retreat——re热(拼音)treat款待(熟词)——过分热情的款待使他退却——He retreated hastily back to his car.——他迅速撤回到车里。 2202——gap——[gp]——n.间隔&#xff0c;——map——map地图——地图上…

leetcode:217. 存在重复元素(先排序再比较邻位)

一、题目&#xff1a; 函数原型&#xff1a; bool containsDuplicate(int* nums, int numsSize) 参数分析&#xff1a; nums是传入的数组 numsSize是传入数组的元素个数 二、思路&#xff1a; 根据题意&#xff0c;判断数组中是否存在出现两次以上的元素。可以先将数组排序&…

线性代数的本质笔记

课程来自b站发现的《线性代数的本质》&#xff0c;可以帮助从直觉层面理解线性代数的一些基础概念&#xff0c;以及把一些看似不同的数学概念解释之后&#xff0c;发现其实有内在的关联。 这里只对部分内容做一个记录&#xff0c;完整内容请自行观看视频~ 01-向量究竟是什么 …

如何进行自动化测试,提高测试效率?

作为测试人员&#xff0c;在进行比较大的项目时&#xff0c;使用自动化测试能帮助我们事半功倍地完成测试工作&#xff0c;提高测试效率&#xff0c;缩短开发周期。 Eolink Apikit 为测试工程师提供 API 文档管理、快速接口调试、测试用例管理、及自动化测试等功能。协作测试工…

公司电脑监控软件|管控企业U盘,防止员工利用U盘泄密

德人合科技——电脑监控软件可以通过U盘管理系统管控企业U盘&#xff0c;防止员工利用U盘泄密。 PC访问地址&#xff1a;https://isite.baidu.com/site/wjz012xr/2eae091d-1b97-4276-90bc-6757c5dfedee 其具体功能如下&#xff1a; U盘接入管控&#xff1a;单位内电脑能否使用U…

同样是发朋友圈,为什么我发的朋友圈没有效果呢?如何做好朋友圈营销呢?

随着手机的发展&#xff0c;越来越多的营销不再局限在电视、电脑&#xff0c;也开始转往移动即时聊天软件。微信承载大量使用者&#xff0c;因此许多企业将营销的目光移到微信的身上。 但是实际操作才发现&#xff0c;营销过程中出现各种各样的问题&#xff0c;最常见的是投入大…

编程题总结 --- 2018

&#xff08;1&#xff09;输入一串字符串&#xff0c;字符串以“#”结尾&#xff0c;判断输入的字符串中0至9的个数。 #include<iostream>using namespace std;int main(){int sum 0;string s;while(cin >> s){if(s "#") break;int n s.size();for(…

10月8日 Jdbc(1)

jdbc 接口是一个类的父类 java连接数据库, java操作数据库, 把java作为数据库的一个客户端 JDBC是接口&#xff0c;而JDBC驱动才是接口的实现&#xff0c;没有驱动无法完成数据库连接&#xff01;每个数据库厂商都有自己的驱动&#xff0c;用来连接自己公司的数据库。 ​ …

AWS SAA-C03考试知识点整理

S3&#xff1a; 不用于数据库功能 分类&#xff1a; S3 Standard &#xff1a;以便频繁访问 S3 Standard-IA 或 S3 One Zone-IA &#xff1a; 不经常访问的数据 Glacier&#xff1a; 最低的成本归档数据 S3 Intelligent-Tiering智能分层 &#xff1a;存储具有不断变化或未知访问…

网络安全副业如何年入数十万 (如何让你的副业超过主页)

安全从业经历与副业经验画了一张“网络安全之副业有道”的思维导图”&#xff0c;该图随着认知提高&#xff0c;将继续丰富完善&#xff0c;共同交流学习。 任何学习的过程一定是要有正向反馈的&#xff0c;如技能的提升、圈内的名气、输出知识带来的经济收入&#xff0c;等等…

如何使用Inno Setup将可执行文件.exe和它的依赖文件及文件夹打包成一个可以安装的.exe文件

环境: Inno Setup 6.2.2 rustdesk编译文件和依赖 问题描述: 如何使用Inno Setup将可执行文件.exe和它的依赖文件及文件夹打包成一个可以安装的.exe文件 解决方案: 一、创建编译脚本 1.新建脚本 下一步 2.填写程序名称版本等信息 3.设置安装默认目录和运行用户更改 选…

什么是SOI

在芯片制程中&#xff0c;经常会听到“SOI”这个名词。而芯片制造上也通常使用SOI衬底制造集成电路。SOI衬底的独特结构可以大大提高芯片的性能&#xff0c;那么SOI到底是什么&#xff1f;有哪些优点&#xff1f;应用在哪些领域&#xff1f;如何制造&#xff1f; 什么是SOI衬底…

SystemVerilog Assertions应用指南 Chapter 1.14蕴含操作符

1.14蕴含操作符 属性p7有下列特别之处 (1)属性在每一个时钟上升沿寻找序列的有效开始。在这种情况下,它在每个时钟上升沿检查信号“a”是否为高。 (2)如果信号“a”在给定的任何时钟上升沿不为高,检验器将产生一个错误信息。这并不是一个有效的错误信息,因为我…

Next.js和sharp实现占位图片生成工具

占位图片&#xff08;Placeholder Image&#xff09; 是前端开发中常用的工具&#xff0c;用于在网页加载慢或未加载完整的情况下&#xff0c;为图像元素提供占位。但是&#xff0c;有时候我们需要更灵活的方式来生成自定义占位图片以满足特定需求。在这篇博客中&#xff0c;我…

ArGIS Engine专题(14)之GP模型根据导入范围与地图服务相交实现叠置分析

一、结果预览 二、需求简介 前端系统开发时,可能遇到如下场景,如客户给出一个图斑范围,导入到系统中后,需要判断图斑是否与耕地红线等地图服务存在叠加,叠加的面积有多少。虽然arcgis api中提供了相交inserect接口,但只是针对图形几何之间的相交,如何要使用该接口,则需…

文件对比工具Beyond Compare 4(4.4.7) for Mac

Beyond Compare 4 是一款强大的文件和文件夹比较工具。它提供了一个直观的界面&#xff0c;使您可以快速比较和同步文件和文件夹。 Beyond Compare 4 具有许多有用的功能&#xff0c;包括比较和合并文件、文件夹和压缩文件&#xff0c;以及同步文件和文件夹。它支持各种类型的文…

C++新经典 | C++ 查漏补缺(STL标准模板库)

目录 一、STL总述 1.容器 &#xff08;1&#xff09;顺序容器 &#xff08;2&#xff09;关联容器 &#xff08;3&#xff09;无序容器 &#xff08;4&#xff09;常用容器 &#xff08;4.1&#xff09;array 数组 &#xff08;4.2&#xff09;vector &#xff08;4.3…

软件功能测试的6种方法

对于测试人员而言&#xff0c;软件产品每个按钮的功能是否准确&#xff0c;链接是否能正常跳转&#xff0c;搜索时会不会出现页面错误&#xff0c;验证并减少这些软件使用过程中可能出现的各种小问题都是功能测试的内容。而对于用户而言&#xff0c;功能能否正常执行都是非常直…