Skip to content

20. [dbproduitscategories] 数据库访问 Web 服务的安全防护

20.1. 搭建工作环境

我们将通过以下项目来实现Web服务的安全防护:

  
  • [spring-security-*]项目位于[<exemples>\spring-database-generic\spring-security]文件夹中;
  • 将为 SGBD 和 MySQL 项目配置安全措施,先部署 [DAO / JDBC] 层,再部署 [DAO / JPA / Hibernate] 层;
  • 按 Alt-F5 键,然后重新生成所有 Maven 项目;

我们需要在 [dbproduitscategories] 数据库中创建用户。为此,请使用运行配置 [spring-security-create-users-hibernate-eclipselink]:

执行此配置会将表 [dbproduitscategories] 的数据填充到表 [USERS, ROLES, USERS_ROLES] 中:

 

生成的标识符 [login/passwd] 如下:[admin/admin]、[user/user]、[guest/guest]。在数据库中,密码是加密的。

Image

完成上述操作后,请执行名为 [spring-security-server-jpa-generic-hibernate-eclipselink] 的运行配置,该配置将启动安全 Web 服务(需先启动 MySQL):

然后执行名为 [spring-security-client-generic-JUnitTestDao] 的运行配置,该配置用于测试安全 Web 服务:

测试应成功。

20.2. Eclipse 项目 [spring-security-server-jdbc-generic]

安全 Web 服务由项目 [spring-security-server-jdbc-generic] 实现:

上文所述:

  • [DAO1] 层即为 [DAO] 层,该层管理 [dbproduitscategories] 数据库中的 [PRODUITS] 和 [CATEGORIES] 表。 该层已编写完成;
  • [DAO2] 层是 [DAO] 层,该层管理数据库 [dbproduitscategories] 中的表 [USERS]、 [ROLES] 和 [USERS_ROLES] 表的 [dbproduitscategories] 层。该层尚待编写;

  

20.2.1. Maven 配置

项目 [spring-security-server-jdbc-generic] 是一个由以下 [pom.xml] 文件配置的 Maven 项目:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-security-server-jdbc-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>spring-security-server-jdbc-generic</name>
    <description>démo spring security</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Web 服务器 / jSON -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-webjson-server-jdbc-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- 插件 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • 第 29-33 行:沿用现有配置,包含已研究的 Web 服务 / json / jdbc 存档;
  • 第 24-27 行:引入 Spring Security 类所需的依赖项;

最终,该项目对Eclipse中加载的其他项目具有以下依赖关系:

  

20.2.2. [DAO2] 层

如上:

  • 层 [DAO1] 即为管理数据库 [dbproduitscategories] 中表 [PRODUITS] 和 [CATEGORIES] 的层 [DAO]。 它已经编写好了;
  • [DAO2] 层是 [DAO] 层,该层管理数据库 [DAO2] 中的表 [USERS]、 [ROLES] 和 [USERS_ROLES] 表的 [dbproduitscategories] 层。这就是我们接下来要编写的内容;
  

Spring Security 要求创建一个实现以下接口的类:

 

此接口在此由类 [AppUserDetails] 实现:


package spring.security.dao;

import generic.jdbc.config.ConfigJdbc;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import spring.jdbc.entities.Role;
import spring.jdbc.entities.User;
import spring.jdbc.infrastructure.DaoException;

public class AppUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    // JdbcTemplate
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    // 属性
    private User user;
    private String simpleClassName = getClass().getSimpleName();

    // 构造函数
    public AppUserDetails() {
    }

    public AppUserDetails(User user, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
        this.user = user;
        this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
    }

    // -------------------------接口
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
...

    // 私有方法----------------
    private List<Role> getRoles(Long id) {
        try {
            // 通过用户ID查找用户
            return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ROLES_BYUSERID, Collections.singletonMap("id", id),    new ShortRoleMapper());
        } catch (Exception e) {
            //e.printStackTrace();
            throw new DaoException(167, e, simpleClassName);
        }
    }

}

// --------------------- 映射器
class ShortRoleMapper implements RowMapper<Role> {

    @Override
    public Role mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Role(rs.getLong("r_ID"), rs.getLong("r_VERSIONING"), rs.getString("r_NAME"));
    }
}
  • 第 22 行:类 [AppUserDetails] 实现了接口 [UserDetails];
  • 第 29-30 行:该类封装了一个用户(第 19 行)以及用于获取该用户详细信息的存储库(第 20 行);
  • 第 27 行:将通过 JDBC 访问数据库,具体使用 [spring-jdbc-generic-04] 项目中定义的 [NamedParameterJdbcTemplate namedParameterJdbcTemplate] 对象。 需要注意的是,该对象并非像通常那样由Spring注入。它是通过第36-39行的构造函数提供的。为什么?因为类[AppUserDetails]不是Spring组件(缺少@Component注解),因此无法对其进行注入;
  • 第36-39行:通过用户及其存储库实例化该类的构造函数;
  • 第 42-49 行:实现接口 [UserDetails] 中的方法 [getAuthorities]。该方法需构建一个由 [GrantedAuthority] 类型或其派生类型元素组成的集合。 此处,我们使用派生类型 [SimpleGrantedAuthority](第 46 行),该类型封装了第 29 行中用户的一个角色的名称;
  • 第45-47行:遍历第29行用户角色的列表,以构建一个[SimpleGrantedAuthority]类型的元素列表;
  • 第45行:为获取用户的角色,调用第53行中的私有方法[getRoles];
  • 第56行:执行以下命令 SQL [ConfigJdbc.SELECT_ROLES_BYUSERID](定义在 [Configjdbc] 中):

public static final String SELECT_ROLES_BYUSERID = "SELECT DISTINCT r.ID as r_ID, r.VERSIONING as r_VERSIONING, r.NAME as r_NAME FROM ROLES r, USERS u, USERS_ROLES ur WHERE u.ID=:id AND ur.USER_ID=u.ID AND ur.ROLE_ID=r.ID";

该查询 SQL 对三个表 [USERS, ROLES, USERS_ROLES] 进行连接,以获取通过主键标识的用户角色。其参数由要查询角色的用户的主键 [:id] 设定。

  • 第56行:[SELECT]查询的每行结果都会通过第66-72行的[ShortRowMapper]类转换为[Role]实体;

让我们回到类 [AppUserDetails] 的代码:


package spring.security.dao;

...

public class AppUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    // JdbcTemplate
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    // 属性
    private User user;
    private String simpleClassName = getClass().getSimpleName();

    // 构造函数
    public AppUserDetails() {
    }

    public AppUserDetails(User user, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
        this.user = user;
        this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
    }

    // -------------------------接口
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

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

    @Override
    public String getUsername() {
        return user.getLogin();
    }

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

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

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

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

    // 获取器和设置器
    ...
}
  • 第35-37行:实现了接口[UserDetails]中的方法[getPassword]。将第12行的用户密码返回;
  • 第39-42行:实现了接口[UserDetails]中的方法[getUserName]。返回第12行用户的登录名;
  • 第44-47行:用户账户永不过期;
  • 第49-52行:用户账户永不被锁定;
  • 第54-57行:用户凭证永不过期;
  • 第59-62行:用户账户始终处于活动状态;

Spring Security 还要求存在一个实现 [AppUserDetailsService] 接口的类:

 

该接口由以下 [AppUserDetailsService] 类实现:


package spring.security.dao;

import generic.jdbc.config.ConfigJdbc;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
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;

import spring.jdbc.entities.User;
import spring.jdbc.infrastructure.DaoException;

@Service
public class AppUserDetailsService implements UserDetailsService {

    // 注入
    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    // 局部
    private String simpleClassName = getClass().getSimpleName();

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        List<User> users;
        try {
            // 通过登录名查找用户
            users = namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_USER_BYLOGIN,
                    Collections.singletonMap("login", login), new ShortUserMapper());
        } catch (Exception e) {
            throw new DaoException(145, e, simpleClassName);
        }
        // 找到了吗?
        if (users.size() == 0) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // 返回用户详细信息
        return new AppUserDetails(users.get(0), namedParameterJdbcTemplate);
    }
}

// --------------------- 映射器
class ShortUserMapper implements RowMapper<User> {

    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new User(rs.getLong("u_ID"), rs.getLong("u_VERSIONING"), rs.getString("u_NAME"), rs.getString("u_LOGIN"),
                rs.getString("u_PASSWORD"));
    }
}
  • 第 21 行:该类将作为 Spring 组件;
  • 第 25-26 行:将通过 JDBC 访问数据库,使用在 [spring-jdbc-generic-04] 项目 Bean 中定义的 [NamedParameterJdbcTemplate namedParameterJdbcTemplate] 对象;
  • 第31-49行:实现接口[UserDetailsService](第22行)中的方法[loadUserByUsername]。参数为用户登录名;
  • 第36-37行:通过用户登录名进行用户查询。SQL [ConfigJdbc.SELECT_USER_BYLOGIN]的顺序如下:

public static final String SELECT_USER_BYLOGIN = "SELECT u.ID as u_ID, u.VERSIONING as u_VERSIONING, u.NAME as u_NAME,u.LOGIN as u_LOGIN,u.PASSWORD as u_PASSWORD FROM USERS u WHERE u.LOGIN= :login";

由 SELECT 生成的每一行,都会通过第 52-58 行中的 [ShortUserMapper] 类转换为 [User] 实体。

  • 第 42-44 行:若未找到,则抛出异常;
  • 第 46 行:构建并渲染一个 [AppUserDetails] 对象。该对象确实属于 [UserDetails] 类型(第 32 行)。向其构造函数传递两项信息:
    • 已找到的用户;
    • [namedParameterJdbcTemplate] 对象,该对象将使 [AppUserDetails] 类能够查询数据库;

20.2.3. [web] 层

项目 [spring-security-server-jdbc-generic] 依赖于项目 [spring-webjson-server-jdbc-generic]:

  

正是该项目实现了 [web] 层。该层无需修改。

20.2.4. 项目的安全配置

该项目由以下类 [AppConfig] 进行配置:

1
  

我们之前已经接触过一个 Spring Security 配置类(参见第 19.2.4 节):


package hello;

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.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}

我们将采用相同的步骤:

  • 第 11 行:定义一个继承自 [WebSecurityConfigurerAdapter] 类的类;
  • 第 13 行:定义方法 [configure(HttpSecurity http)],用于定义 Web 服务中各个 URL 的访问权限;
  • 第19行:定义方法[configure(AuthenticationManagerBuilder auth)],用于定义用户及其角色;

[AppConfig] 类将如下所示:


package spring.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import spring.security.dao.AppUserDetailsService;
import spring.webjson.server.config.WebConfig;

@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = { "spring.security.dao", "spring.security.service" })
@Import({ spring.webjson.server.config.AppConfig.class, WebConfig.class })
public class AppConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AppUserDetailsService appUserDetailsService;

    // 安全防护
    private boolean activateSecurity = true;

    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // 身份验证由 Bean 完成 [appUserDetailsService]
        // 密码由哈希算法加密 BCrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // 安全应用?
        if (activateSecurity) {
            // 密码通过 Authorization: Basic xxxx 头部传递
            http.httpBasic();
            // 必须为所有人授权 HTTP OPTIONS 方法
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // 仅 ADMIN 角色可使用该应用程序
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // 所有 URL
                    .hasRole("ADMIN");
            // 是否需要会话?
            //http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • 第17行:该类是一个Spring配置类;
  • 第18行:启用Spring Security组件;
  • 第26行:获取[DAO2]层以及[spring.security.service]包中的Spring组件,后者将在后续内容中讨论;
  • 第 23 行:导入 [spring-webjson-server-jdbc-generic] 项目的 Bean,该项目实现了 [web] 层。其中还包含 [DAO1] 层的 Bean;
  • 第 22-23 行:注入了 [AppUserDetails] 类,该类为应用程序用户提供访问权限;
  • 第 26 行:一个布尔值,用于控制 Web 应用程序是否处于安全状态(true)或非安全状态(false);
  • 第28-33行:方法[configure(HttpSecurity http)]定义用户及其角色。该方法接收一个类型为[AuthenticationManagerBuilder]的参数。该参数包含两项信息(第32行):
    • 对第23行服务[appUserDetailsService]的引用,该服务提供对已注册用户的访问权限。需注意的是,此处并未显示用户是否存储在数据库中。因此,这些用户可能存储在缓存中,或由Web服务提供……
    • 密码所使用的加密类型。我们采用了BCrypt算法;
  • 第35-53行:方法[configure(HttpSecurity http)]定义了Web服务中URL的访问权限;
  • 第 38 行:我们在入门项目中看到,默认情况下 Spring Security 会管理一个 CSRF 令牌(跨站请求伪造),需要进行身份验证的用户必须将该令牌发回给服务器。 此处该机制已被禁用。结合布尔值(isSecured=false),这使得可以不使用安全机制来运行Web应用程序;
  • 第 42 行:启用 HTTP 标头认证模式。客户端需发送以下 HTTP 标头:
Authorization:Basic code

其中 code 是字符串 login:password 通过 Base64 算法编码后的结果。例如,字符串 admin:admin 的 Base64 编码为 YWRtaW46YWRtaW4=。 因此,用户名 [admin] 和密码 [admin] 将发送以下标头 HTTP 进行身份验证:

Authorization:Basic YWRtaW46YWRtaW4=
  • 第 47-49 行:表示拥有角色 [ROLE_ADMIN] 的用户可以访问 Web 服务中的所有 URL。这意味着没有该角色的用户无法访问该 Web 服务;
  • 第 51 行:在 [session] 模式下,用户完成一次身份验证后,后续访问无需再次验证。这是 Spring Security 的默认设置。 第 51 行会禁用此模式。若该行生效,用户每次访问都需进行身份验证。由于无会话状态下受保护 Web 服务的响应速度低于有会话状态,因此第 51 行已被注释掉;

20.2.5. 安全 Web 服务测试

我们将使用 Chrome 客户端 [Advanced Rest Client] 测试 Web 服务。我们需要指定 HTTP 身份验证头:

Authorization:Basic code

其中 [code] 是字符串 [login:password] 的 Base64 编码。要生成此代码,可以使用 [spring-security-create-users] 项目中的以下程序:

  

package spring.security.helpers;

import org.springframework.security.crypto.codec.Base64;

public class Base64Encoder {

    public static void main(String[] args) {
        // 需要两个参数:登录名 密码
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // 获取这两个参数
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // 对字符串进行编码
        byte[] data = Base64.encode(chaîne.getBytes());
        // 显示其 Base64 编码
        System.out.println(new String(data));
    }

}

如果我们使用两个参数 [admin admin] 运行该程序:

  

将得到以下结果:

YWRtaW46YWRtaW4=

现在我们可以进行测试了:

  • 需启动 SGBD 和 MySQL;
  • 我们将名为 [spring-jdbc-generic-04-fillDataBase] 的运行配置填入 [PRODUITS] 和 [CATEGORIES] 表中:
 
  • 如果尚未执行,我们将使用名为 [spring-security-create-users-hibernate-eclipselink] 的运行配置填充表 [USERS, ROLES, USERS_ROLES]:
 
  • 我们使用名为 [spring-security-server-jdbc-generic] 的运行配置启动安全 Web 服务:
 

随后,使用 Chrome 客户端 [Advanced Rest Client],我们请求所有类别的详细版本:

  • 在 [1] 中,我们请求详细分类的 URL;
  • 在 [2] 中,使用 GET 方法;
  • 在 [3] 中,我们提供身份验证的 HTTP 标头。代码 [YWRtaW46YWRtaW4=] 是字符串 [admin:admin] 的 Base64 编码;
  • 在 [4] 中,我们发送命令 HTTP;

服务器的响应如下:

  • 在 [1] 中,包含认证头部 HTTP;
  • 在 [2] 中,服务器返回响应 jSON;

确实获得了分类列表:

 

现在尝试发送一个带有错误认证头部的 HTTP 请求。响应如下:

  • 在 [1] 中:认证头 HTTP;

我们得到以下响应:

  • 转换为 [2]:Web 服务的响应;

现在,我们尝试用户 user / user。该用户存在,但无权访问 Web 服务。如果我们使用两个参数 [user user] 运行 Base64 编码程序:

  

将得到以下结果:

dXNlcjp1c2Vy
  • 在 [1] 中:认证头 HTTP 错误;
  • 在 [2] 中:Web 服务的响应。它与之前的 [401 Unauthorized] 不同。这次,用户已成功认证,但没有足够的权限访问 URL;

一个安全的Web服务现已投入运行。

20.2.6. 一个 URL 身份验证

  

我们将创建一个 URL,用于判断用户是否被授权访问 Web 服务。为此,我们创建以下新的控制器 MVC [AuthenticateController]:


package spring.security.service;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import spring.webjson.server.service.Response;

@RestController
public class AuthenticateController {
    @RequestMapping(value = "/authenticate", method = RequestMethod.GET)
    public Response<Void> authenticate() {
        return new Response<Void>(0, null, null);
    }

}
  • 第 9 行:类 [AuthenticateController] 是一个 Spring 控制器。因此,它暴露了 URL。 注解 [@RestController] 表示处理这些 URL 的方法会自行向客户端返回响应;
  • 第 11 行:暴露 URL 和 [/authenticate];
  • 第 12-14 行:该方法仅返回一个空的 [Response] 对象,但其 [status] 字段值为 0,表明未发生错误;

这个 URL 有什么用?当我们仅需对用户进行身份验证时,会调用它。我们已经看到,如果安全层不接受该用户,它会抛出异常。以下是一个示例;

使用用户 [admin:admin]:

返回的是空响应,但没有抛出异常。

使用用户 [user:user]:

发生了异常。

20.2.7. 结论

在无需修改原始 Web/JSON 项目的情况下,成功为 Spring Security 添加了必要的类。这种非常理想的情况源于数据库中新增的三个表与现有表相互独立。 甚至可以将它们放在一个独立的数据库中。在其他情况下,新增的表可能与现有表存在关联。此时,现有 [DAO] 层的代码需要进行审查。

20.3. 为安全 Web 服务 / jSON 编写的客户端

我们已经为非安全 Web 服务 / jSON 编写了一个客户端:

现在我们将编写一个针对安全 Web 服务的客户端:

  

20.3.1. [Client HTTP] 层

 

类 [Client] 负责与安全 Web 服务器 / HTTP 进行通信。 正如我们刚才所见,在此通信中,客户端现在必须发送一个身份验证头,例如:

Authorization:Basic YWRtaW46YWRtaW4=

[IClient] 接口变为如下形式:


package spring.webjson.client.dao;

import org.springframework.http.HttpMethod;

import spring.webjson.client.entities.Credentials;

public interface IClient {
    public <T1, T2> T1 getResponse(Credentials credentials,String url, HttpMethod method, int errStatus, T2 body);
}
  • 第 8 行:方法 [getResponse] 的第一个参数现为一个 [Credentials] 对象,该对象封装了用户的身份信息:

package spring.security.client.entities;

public class Credentials {

    // 属性
    private String login;
    private String password;

    // 构造函数
    public Credentials() {
    }

    public Credentials(String login, String password) {
        this.login = login;
        this.password = password;
    }

    // 获取器和设置器
...
}

实现 [IClient] 接口的 [Client] 类发生如下变化:


package spring.security.client.dao;

...

@Component
public class Client implements IClient {

    // 注入
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // 局部变量
    private String simpleClassName = getClass().getSimpleName();

    private String getBase64(Credentials credentials) {
        // 将用户名和密码进行 Base64 编码 - 需要 Java 8
        String chaîne = String.format("%s:%s", credentials.getLogin(), credentials.getPassword());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
    }

    // 通用请求
    @Override
    public <T1, T2> T1 getResponse(Credentials credentials, String url, HttpMethod method, int errStatus, T2 body) {
        // 服务器响应
        ResponseEntity<Response<T1>> response;
        try {
            // 准备请求
            RequestEntity<?> request = null;
            if (method == HttpMethod.GET) {
                HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))    .accept(MediaType.APPLICATION_JSON);
                if (credentials != null) {
                    headersBuilder = headersBuilder.header("Authorization", getBase64(credentials));
                }
                request = headersBuilder.build();
            }
            if (method == HttpMethod.POST) {
                BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url))).header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
                if (credentials != null) {
                    bodyBuilder = bodyBuilder.header("Authorization", getBase64(credentials));
                }
                request = bodyBuilder.body(body);
            }
            // 执行请求
            response = restTemplate.exchange(request, new ParameterizedTypeReference<Response<T1>>() {
            });
        } catch (Exception e) {
            // 封装异常
            throw new DaoException(errStatus, e, simpleClassName);
        }
...
    }
...
}
  • 第 33-35 行、第 40-42 行:如果用户 [credentials] 不为空,则添加身份验证头。 用户及其密码的编码由第17-21行的[getBase64]方法负责。 需注意,该方法使用了一个属于 JDK 1.8 的 [Base64] 类。我们的 HTTP 客户端可与非安全 Web 服务配合使用。 只需向其传递一个等于 null 的 [credentials];
  • 除上述行外,代码保持不变;

20.3.2. [DAO] 层

20.3.2.1. [IDao] 接口

  

项目 [spring-webjson-client-generic] 中接口 [IDao] 的所有方法都接收一个额外参数 [Credentials credentials]:


package spring.security.client.dao;

import java.util.List;

import spring.security.client.entities.AbstractCoreEntity;
import spring.security.client.entities.Credentials;

public interface IDao<T extends AbstractCoreEntity> extends IAuthenticate {

    // 所有 T 实体的列表
    public List<T> getAllShortEntities(Credentials credentials);

    public List<T> getAllLongEntities(Credentials credentials);

    // 特定实体的列表 - 简短版本
    public List<T> getShortEntitiesById(Credentials credentials, Iterable<Long> ids);

    public List<T> getShortEntitiesById(Credentials credentials, Long... ids);

    public List<T> getShortEntitiesByName(Credentials credentials, Iterable<String> names);

    public List<T> getShortEntitiesByName(Credentials credentials, String... names);

    // 特定实体的列表 - 长版
    public List<T> getLongEntitiesById(Credentials credentials, Iterable<Long> ids);

    public List<T> getLongEntitiesById(Credentials credentials, Long... ids);

    public List<T> getLongEntitiesByName(Credentials credentials, Iterable<String> names);

    public List<T> getLongEntitiesByName(Credentials credentials, String... names);

    // 更新多个实体
    public List<T> saveEntities(Credentials credentials, Iterable<T> entities);

    public List<T> saveEntities(Credentials credentials, @SuppressWarnings("unchecked") T... entities);

    // 删除所有实体
    public void deleteAllEntities(Credentials credentials);

    // 删除多个实体
    public void deleteEntitiesById(Credentials credentials, Iterable<Long> ids);

    public void deleteEntitiesById(Credentials credentials, Long... ids);

    public void deleteEntitiesByName(Credentials credentials, Iterable<String> names);

    public void deleteEntitiesByName(Credentials credentials, String... names);

    public void deleteEntitiesByEntity(Credentials credentials, Iterable<T> entities);

    public void deleteEntitiesByEntity(Credentials credentials, @SuppressWarnings("unchecked") T... entities);
}
  • 第 8 行:接口 [IDao] 继承了以下接口 [IAuthenticate]:

package spring.security.client.dao;

import spring.security.client.entities.Credentials;

public interface IAuthenticate {
    // 身份验证
    public void authenticate(Credentials credentials);
}

接口 [IAuthenticate] 仅包含唯一的方法 [authenticate]。如果用户 [Credentials credentials] 被安全 Web 服务接受,该方法不返回任何值(void);否则抛出异常。

20.3.2.2. 类 [AbstractDao]

  

需要指出的是,[AbstractDao]类是[DaoCategorie]类的父类,后者管理产品类别的URL,而[DaoProduit]类则管理产品的URL。项目 [spring-webjson-client-generic] 中类 [AbstractDao] 的所有方法都会接收一个额外的参数 [Credentials credentials],并将该参数传递给子类。以下是一个示例:


    @Override
    public List<T1> getShortEntitiesById(Credentials credentials, Iterable<Long> ids) {
        // 参数有效性
        List<T1> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // 结果
        return getShortEntitiesById(credentials, Lists.newArrayList(ids));
}
  • 方法 [getShortEntitiesById] 接收参数 [Credentials credentials](第 2 行),并将其传递(第 9 行)给子类的 [getShortEntitiesById] 方法;

类 [AbstractDao] 的骨架如下:


package spring.security.client.dao;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;

import spring.security.client.entities.AbstractCoreEntity;
import spring.security.client.entities.Credentials;
import spring.security.client.infrastructure.MyIllegalArgumentException;

import com.google.common.collect.Lists;

public abstract class AbstractDao<T1 extends AbstractCoreEntity> implements IDao<T1> {

    @Autowired
    private IAuthenticate authenticate;

...
}
  • 第 14 行:该类实现了我们之前描述的 [IDao] 接口;
  • 第 16-17 行:注入了一个 [IAuthenticate] 接口的实例。该接口由以下 [Authenticate] 类实现:

package spring.security.client.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;

import spring.security.client.entities.Credentials;

@Component
public class Authenticate implements IAuthenticate{
    @Autowired
    protected IClient client;

    // 验证 [credentials,mdp]
    public void authenticate(Credentials credentials) {
        client.<Void, Void> getResponse(credentials, "/authenticate", HttpMethod.GET, 111, (Void) null);
    }

}
  • 第 9 行:类 [Authenticate] 是 Spring 组件;
  • 第 10 行:该类实现了 [IAuthenticate] 接口;
  • 第 11-12 行:注入客户端 HTTP,用于与安全 Web 服务进行交互;
  • 第15-17行:实现了接口中的[authenticate]方法;
  • 第16行:向URL [/authenticate]发送了一条HTTP GET命令。 第 20.2.6 节已介绍了该 URL 的用法。其原理是:如果用户 [credentials] 未知或权限不足,则调用将引发异常;

类 [AbstractDao] 以如下方式实现了接口 [IDao] 中的方法 [authenticate]:


    @Autowired
    private IAuthenticate authenticate;

    @Override
    public void authenticate(Credentials credentials) {
        authenticate.authenticate(credentials);
}
  • 第 7 行:任务被委托给类 [Authenticate] 的方法 [authenticate]。因此,如果用户 [Credentials credentials] 未被安全 Web 服务接受,则会抛出异常;

20.3.2.3. 类 [DaoCategorie, DaoProduit]

  

[DaoCategorie, DaoProduit]类是[spring-webjson-server-generic]项目的类,带有额外的参数[Credentials credentials]。以下是一个示例:


@Component
public class DaoCategorie extends AbstractDao<Categorie> {

    // 注入
    @Autowired
    protected ApplicationContext context;
    @Autowired
    protected IClient client;

    @Override
    public List<Categorie> getAllShortEntities(Credentials credentials) {
        try {
            // 过滤器jSON
            ObjectMapper mapper = context.getBean("jsonMapperShortCategorie", ObjectMapper.class);
            // 获取所有分类
            Object map = client.<List<Categorie>, Void> getResponse(credentials, "/getAllShortCategories", HttpMethod.GET,
                    202, null);
            // 类别列表 List<Categorie>
            return mapper.readValue(mapper.writeValueAsString(map), new TypeReference<List<Categorie>>() {
            });
        } catch (DaoException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new DaoException(221, e2, simpleClassName);
        }
    }
....

20.3.3. Spring配置

  

类 [AppConfig] 用于配置项目的 Spring 环境。它与 [spring-webjson-client-generic] 项目中的配置完全相同,仅有一处细微差别:


@Configuration
@ComponentScan({ "spring.security.client.dao" })
public class AppConfig {
  • 第 2 行:需填写新层 [DAO] 的包名;

20.3.4. [DAO] 层的测试

  

20.3.4.1. 测试 [JUnitTestCredentials]

[JUnitTestCredentials]测试使用[IDao.authenticate]方法来验证某些用户的有效性:


package client.tests.junit;

import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import spring.security.client.config.AppConfig;
import spring.security.client.dao.IAuthenticate;
import spring.security.client.entities.Credentials;
import spring.security.client.infrastructure.DaoException;

@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestCredentials {

    // 层 [DAO]
    @Autowired
    private IAuthenticate authenticate;

    // 用户
    static private Credentials admin;
    static private Credentials user;
    static private Credentials unknown;

    @BeforeClass
    public static void init() {
        admin = new Credentials("admin", "admin");
        user = new Credentials("user", "user");
        unknown = new Credentials("x", "y");
    }

    @Test()
    public void checkUserUser() {
        DaoException se = null;
        try {
            authenticate.authenticate(user);
        } catch (DaoException e) {
            se = e;
            System.out.println("checkUserUser: " + e);
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("403 Forbidden", se.getExceptions().get(0).getErrorMessage());
    }

    @Test()
    public void checkUserUnknown() {
        DaoException se = null;
        try {
            authenticate.authenticate(unknown);
        } catch (DaoException e) {
            se = e;
            System.out.println("checkUserUnknown : " + e);
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("401 Unauthorized", se.getExceptions().get(0).getErrorMessage());
    }

    @Test()
    public void checkUserAdmin() {
        DaoException se = null;
        try {
            authenticate.authenticate(admin);
        } catch (DaoException e) {
            se = e;
            System.out.println("checkUserAdmin : " + e);
        }
        Assert.assertNull(se);
    }
}
  • 在测试类初始化时(第29-34行),创建了三个用户:
    • 用户 [admin] 拥有访问 Web 服务 URL 的权限。该权限在第 63-72 行进行测试;
    • 用户 [user] 存在,但无权使用 Web 服务的 URL。该用户在第 37-47 行进行测试;
    • 用户 [unknown] 不存在。测试范围为第 50-60 行;

使用名为 [spring-security-server-jdbc-generic] [1] 的运行配置启动安全 Web 服务:

随后使用名为 [spring-security-client-generic-JUnitTestCredentials] [2] 的运行配置启动测试 JUnit [JUnitTestCredentials]。获得的控制台结果如下:

checkUserUser: [code=111, trace=[Client,getResponse,65], exceptions=[{"className":"org.springframework.web.client.HttpClientErrorException","errorMessage":"403 Forbidden"}]
checkUserUnknown : [code=111, trace=[Client,getResponse,65], exceptions=[{"className":"org.springframework.web.client.HttpClientErrorException","errorMessage":"401 Unauthorized"}]

且测试成功:

  

20.3.4.2. 测试 [JUnitTestDao]

测试 [JUnitTestDao] 与非安全项目中的 [spring-webjson-client-generic] 完全相同,,只是现在被测试的 [DAO] 层方法的首个参数均为用户 [admin / admin]:


@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao {

    // Spring 上下文
    @Autowired
    private ApplicationContext context;
    // [DAO] 层
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;

....

    // 用户
    static private Credentials admin;

    @BeforeClass
    public static void init() {
        admin = new Credentials("admin", "admin");
    }

    @Before
    public void clean() {
        // 每次测试前清理数据库
        log("Vidage de la base de données", 1);
        // 清空表 [CATEGORIES] 并级联清空表 [PRODUITS]
        daoCategorie.deleteAllEntities(admin);
        // 清空字典
        for (Long id : mapCategories.keySet()) {
            mapCategories.remove(id);
        }
        for (Long id : mapProduits.keySet()) {
            mapProduits.remove(id);
        }
    }

    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // 填充表
        ...
        // 添加分类 - 产品也将随之
        // 插入 - 同时返回结果
        return daoCategorie.saveEntities(admin, categories);
    }

    private Object[] showDataBase() throws BeansException, JsonProcessingException {
        // 分类列表
        log("Liste des catégories", 2);
        List<Categorie> categories = daoCategorie.getAllShortEntities(admin);
        affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
        // 产品列表
        log("Liste des produits", 2);
        List<Produit> produits = daoProduit.getAllShortEntities(admin);
        affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
        // 结果
        return new Object[] { categories, produits };
    }
...

所有操作均由用户 [admin / admin] 执行,该用户是唯一拥有安全 Web 服务访问权限的用户。

我们将使用名为 [spring-security-client-generic-JUnitTestDao] 的运行配置启动测试:

 

测试通过,但可以发现其速度比非安全Web服务更慢。应用程序的安全化会显著增加其响应时间。在安全Web服务的性能方面,有一个重要因素值得注意:在其配置类[AppConfig]中,我们写道:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // 安全应用?
        if (activateSecurity) {
            // 密码通过 Authorization: Basic xxxx 头部传递
            http.httpBasic();
            // 必须为所有人授权 HTTP OPTIONS 方法
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // 仅 ADMIN 角色可使用该应用程序
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // 所有 URL
                    .hasRole("ADMIN");
            // 无会话
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
}

第17行代码会产生性能开销。它决定了用户是否需要在每次访问时进行身份验证。如果将其注释掉,JUnit测试的耗时将明显缩短, 这是因为用户 [admin] 仅在首次测试时进行身份验证,后续测试则无需验证(即使客户端发送了身份验证头 HTTP,服务器也不会重新验证用户的密码)。

20.4. Eclipse 项目 [spring-security-server-jpa-generic]

安全 Web 服务现将由项目 [spring-security-server-jpa-generic] 实现,该项目基于项目 [spring-jpa-generic],后者使用 Spring Data JPA 管理数据库访问:

上文所述:

  • [DAO1] 层即 [DAO] 层,该层管理数据库 [dbproduitscategories] 中的表 [PRODUITS] 和 [CATEGORIES]。 该层已编写完成;
  • [DAO2] 层是 [DAO] 层,该层管理数据库 [dbproduitscategories] 中的表 [USERS]、 [ROLES] 和 [USERS_ROLES] 表的 [dbproduitscategories] 层。该层尚待编写;

项目 [spring-security-server-jpa-generic] 首先通过复制先前研究过的项目 [spring-security-server-jdbc-generic] 获得。实际上,层 [web] 和 [security] 保持不变,因为:

  • [DAO1 / Repositories / JPA] 层(已编写)与 [DAO1 / JDBC] 层具有相同的接口;
  • 待写的层 [DAO2 / Repositories / JPA] 将与层 [DAO2 / JDBC] 具有相同的接口;

项目 [spring-security-server-jpa-generic] 如下:

  
  • 包 [spring.security.repositories] 实现了层 [repositories];
  • 包 [spring.security.dao] 实现了层 [dao2];

20.4.1. Maven 项目

该项目是一个由以下 [pom.xml] 文件配置的 Maven 项目:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-security-server-jpa-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>spring-security-server-jpa-generic</name>
    <description>démo spring security</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Web 服务器 / jSON -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-webjson-server-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <!-- 插件 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • 第 24-27 行:项目对 [security] 层的依赖;
  • 第29-33行:项目中[web]层的依赖关系。项目[spring-webjson-server-jpa-generic]完全实现了[web]层。该层无需编写或修改;

最终,依赖关系如下:

  

20.4.2. Spring配置

  

前一个项目 [spring-security-server-jdbc-generic] 的配置文件 [AppConfig] 适用。只需向其中添加一项额外配置:


@Configuration
@EnableWebSecurity
@EnableJpaRepositories(basePackages = { "spring.security.repositories" })
@ComponentScan(basePackages = { "spring.security.dao", "spring.security.service" })
@Import({ spring.webjson.server.config.AppConfig.class })
public class AppConfig extends WebSecurityConfigurerAdapter {
  • 第 3 行:声明实现 [repositories] 层的包;
  • 第 4 行:包含 Spring Bean 的包在新项目中使用相同名称;
  • 第 5 行:在之前的项目中,类 [spring.webjson.server.config.AppConfig] 位于依赖项 [spring-webjson-server-jdbc-generic] 中。在此处,它将位于依赖项 [spring-webjson-server-jpa-generic] 中;

20.4.3. JPA 层

由层 [JPA] 管理的实体 JPA 位于项目 [mysql-config-jpa-hibernate] [2] 中,该项目是项目 QZXW2HTMLP005265Z 的依赖项QX:

类 [User] 是表 [USERS] 的映像:

Image

  • ID:主键;
  • VERSION:行版本控制列;
  • IDENTITY:用户的描述性标识;
  • LOGIN:用户的登录名;
  • PASSWORD:其密码;

package generic.jpa.entities.dbproduitscategories;

import generic.jdbc.config.ConfigJdbc;

import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;

import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
@Table(name = ConfigJdbc.TAB_USERS)
public class User implements AbstractCoreEntity {
    // 属性
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;

    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;

    @Transient
    protected EntityType entityType=EntityType.POJO;

    // 属性
    @Column(name = ConfigJdbc.TAB_USERS_NAME, length = 30, nullable = false)
    private String name;
    @Column(name = ConfigJdbc.TAB_USERS_LOGIN, length = 30, unique = true, nullable = false)
    private String login;
    @Column(name = ConfigJdbc.TAB_USERS_PASSWORD, length = 60, nullable = false)
    private String password;

    // 相关的 UserRole
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = { CascadeType.ALL })
    @JsonIgnore
    private List<UserRole> userRoles;

    // 构造函数
    public User() {
    }

    public User(Long id, Long version, String identity, String login, String password) {
        this.id = id;
        this.version = version;
        this.name = identity;
        this.login = login;
        this.password = password;
    }

    // ------------------------------------------------------------
    // 重写 [equals] 和 [hashcode]
...

    // 获取器和设置器
...
}
  • 第 23 行:该类实现了已用于其他实体的 [AbstractCoreEntity] 接口;
  • 第 34-35 行:实体的类型。该属性不会保存在数据库中 [@Transient];
  • 第 38-43 行:用户的三个基本属性(name、login、password);
  • 第 46-48 行:用户的角色列表。用户可能拥有多个角色。同样,我们将看到一个角色可以关联多个用户。 因此,按照JPA中的术语定义,实体[User]与[Role]之间存在一种[ManyToMany]关系:
    • 一个用户可以关联多个角色;
    • 一个角色可以关联多个用户;

该关系[ManyToMany]通过关联表[USERS_ROLES]在数据库中实现。 如果用户 U 与角色 R 存在关联,则在表 [USERS_ROLES] 中记录该关联,并保存实体 (U,R) 的主键对。在 JPA 表中, 连接实体 [User] 和 [Role] 的关系 [ManyToMany] 可以拆分为两个关系 [ManyToOne, OneToMany]:

  • (续)
    • 从实体 [User] 指向实体 [UserRole] 的关系 [ManyToOne];
    • 从实体 [UserRole] 到实体 [UserRole] 的关系 [OneToMany];

同样,连接实体 [Role] 和 [User] 的关系 [ManyToMany] 可以拆分为两个关系 [ManyToOne, OneToMany]:

  • (续)
    • 从实体 [Role] 指向实体 [UserRole] 的关系 [ManyToOne];
    • 从实体 [UserRole] 到实体 [User] 的关系 [OneToMany];
  • 关系 48:用户拥有多个角色的事实通过关系 [OneToMany] 指向实体 [UserRole] 来表示;

类 [Role] 是表 [ROLES] 的映射:

Image

  • ID:主键;
  • VERSION:行版本控制列;
  • NAME:角色名称。默认情况下,Spring Security 期望名称采用 ROLE_XX 的格式,例如 ROLE_ADMIN 或 ROLE_GUEST;

package generic.jpa.entities.dbproduitscategories;

import generic.jdbc.config.ConfigJdbc;

import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;

import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
@Table(name = ConfigJdbc.TAB_ROLES)
public class Role implements AbstractCoreEntity {
    // 属性
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;
    
    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;

    @Transient
    protected EntityType entityType=EntityType.POJO;

    // 属性
    @Column(name = ConfigJdbc.TAB_ROLES_NAME, length = 30, unique = true, nullable = false)
    private String name;

    // 相关的 UserRole
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "role", cascade = { CascadeType.ALL })
    @JsonIgnore
    private List<UserRole> userRoles;

    // 构造函数
    public Role() {
    }

    public Role(Long id, Long version, String name) {
        this.id = id;
        this.version = version;
        this.name = name;
    }

    // 获取器和设置器
    public Role(String name) {
        this.name = name;
    }

    // ------------------------------------------------------------
    // 重写 [equals] 和 [hashcode]
...
    // getter 和 setter
...
}
  • 第 42-44 行:一个角色可以关联多个用户,这通过 [@OneToMany] 与实体 [UserRole] 之间的关系来体现;

类 [UserRole] 是表 [USERS_ROLES] 的映射:

Image

一个用户可以拥有多个角色,一个角色可以包含多个用户。我们有一个多对多关系,由表 [USERS_ROLES] 体现。

  • ID:主键;
  • VERSION:行版本控制列;
  • USER_ID:用户标识;
  • ROLE_ID:角色标识符;

package generic.jpa.entities.dbproduitscategories;

import generic.jdbc.config.ConfigJdbc;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;

@Entity
@Table(name = ConfigJdbc.TAB_USERS_ROLES)
public class UserRole implements AbstractCoreEntity {
    // 属性
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;

    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;

    @Transient
    protected EntityType entityType=EntityType.POJO;

    // 一个 UserRole 引用一个 User
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_USERS_ROLES_USER_ID, nullable = false)
    private User user;

    // 一个 UserRole 引用一个 Role
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_USERS_ROLES_ROLE_ID, nullable = false)
    private Role role;

    // 构造函数
    public UserRole() {

    }

    public UserRole(User user, Role role) {
        this.user = user;
        this.role = role;
    }

    // ------------------------------------------------------------
    // 重写 [equals] 和 [hashcode]
    ...

    // 获取器和设置器
...
}
  • 第 34-36 行:实现表 [USERS_ROLES] 到表 [USERS] 的外键;
  • 第38-41行:实现表[USERS_ROLES]到表[ROLES]的外键;

20.4.4. [repositories]层

  

接口 [UserRepository] 管理对实体 [User] 的访问:


package spring.security.repositories;

import generic.jpa.entities.dbproduitscategories.Role;
import generic.jpa.entities.dbproduitscategories.User;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User, Long> {

    // 按 ID 标识的用户角色列表
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // 按唯一登录名查询用户的角色列表
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // 通过登录名查询用户
    User findUserByLogin(String login);
}
  • 第 9 行:接口 [UserRepository] 继承了 Spring Data 的 [CrudRepository] 接口(第 7 行);
  • 第 12-13 行:方法 [getRoles(long id)] 可获取通过 [id] 标识的用户的全部角色
  • 第16-17行:同上,但针对通过登录名/密码识别的用户;
  • 第 20 行:通过用户名查找用户;

接口 [RoleRepository] 管理对实体 [Role] 的访问:


package spring.security.repositories;

import generic.jpa.entities.dbproduitscategories.Role;

import org.springframework.data.repository.CrudRepository;

public interface RoleRepository extends CrudRepository<Role, Long> {

    // 通过名称搜索角色
    Role findRoleByName(String name);

}
  • 第7行:接口[RoleRepository]扩展了接口[CrudRepository];
  • 第 10 行:可通过名称搜索角色。在此提醒,实体 [Role] 包含字段 [name]。 方法 [findEntityByChamp] 由 Spring Data 自动实现。因此,此处无需实现方法 [finRoleByName],只需在接口中声明即可。

接口 [UserRoleRepository] 管理对实体 [UserRole] 的访问:


package spring.security.repositories;

import generic.jpa.entities.dbproduitscategories.UserRole;

import org.springframework.data.repository.CrudRepository;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • 第 7 行:接口 [UserRoleRepository] 仅继承了接口 [CrudRepository],未为其添加新方法;

20.4.5. 尿布 [DAO2]

  

在 [DAO2] 层中,包含与第 20.2.2 节中先前研究过的 [spring-security-server-jdbc-generic] 项目中的 [DAO2] 层相同的类。 现在只需借助 [repositories] 层中的类来实现它们即可。

[AppUserDetails] 类的演变如下:


package spring.security.dao;

import generic.jpa.entities.dbproduitscategories.Role;
import generic.jpa.entities.dbproduitscategories.User;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import spring.data.infrastructure.DaoException;
import spring.security.repositories.UserRepository;

public class AppUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    // 属性
    private User user;
    private UserRepository userRepository;
    
    // 本地
    private String simpleClassName = getClass().getSimpleName();

    // 构造函数
    public AppUserDetails() {
    }

    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }

    // -------------------------接口
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        Iterable<Role> roles;
        try {
            roles = userRepository.getRoles(user.getId());
        } catch (Exception e) {
            e.printStackTrace();
            throw new DaoException(167, e, simpleClassName);
        }
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
...
}
  • 第 31 行,该类的构造函数接收 [UserRepository] 对象作为第二个参数,这使得该类能够获取特定用户的角色(第 42 行);

Spring 组件 [AppUserDetailsService] 的演变如下:


package spring.security.dao;

import generic.jpa.entities.dbproduitscategories.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;

import spring.data.infrastructure.DaoException;
import spring.security.repositories.UserRepository;

@Service
public class AppUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;
    // 本地
    private String simpleClassName = getClass().getName();

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // 通过登录名查找用户
        User user;
        try {
            user = userRepository.findUserByLogin(login);
        } catch (Exception e) {
            throw new DaoException(168, e, simpleClassName);
        }
        // 找到?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // 返回用户详细信息
        return new AppUserDetails(user, userRepository);
    }

}
  • 第 18 行:注入 Spring 组件 [userRepository],该组件将使服务能够根据登录名识别用户(第 27 行);

最终发现,我们只需要 [userRepository],而无需另外两个仓库 [roleRepository, userRoleRepository]。这两个仓库将在后续项目中使用,旨在填充 [USERS, ROLES, USERS_ROLES] 表。

20.4.6. 测试

使用名为 [spring-security-server-jpa-generic-hibernate-eclipselink] [1] 的配置启动了安全 Web 服务。 通用客户端的 [JUnitTestDao] 测试使用名为 [spring-security-client-generic-JUnitTestDao] [2] 的配置启动:

测试成功。

20.5. Eclipse 项目 [spring-security-create-users]

  

20.5.1. 数据库

项目运行将 [dbproduitscategories] 表的数据填入 [USERS, ROLES, USERS_ROLES] 表:

Image

 

生成的标识符 [login/passwd] 如下:[admin/admin]、[user/user]、[guest/guest]。在数据库中,密码是加密的。

Image

20.5.2. Maven 配置

该项目是一个由以下文件 [pom.xml] 配置的 Maven 项目:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-security-create-users-jpa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-security-create-users-jpa</name>
    <description>création de utilisateurs dans la base [dbproduitscategories]</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- spring-security-server-jpa-generic -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-security-server-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • 第 22-25 行:对 Spring Security 框架的依赖。该框架提供了密码加密算法
  • 第27-31行:依赖于我们刚刚构建的[spring-security-server-jpa-generic]项目。该项目实现了该项目的[repositories]和[JPA]层;

最终,依赖关系如下:

  

20.5.3. [console] 层

由于 [repositories] 和 [JPA] 这两层已由依赖项 [spring-security-server-jpa-generic] 实现,因此只需实现 [console] 这一层。

  
  • [AppConfig] 是该项目的 Spring 配置类;
  • [CreateUsers] 是用于创建用户和角色的可执行类;
  • [Base64Encoder] 是一个辅助类,用于生成 [login, password] 对的 Base64 编码。我们之前已经用过它。它对本项目没有用;

Spring 配置类 [AppConfig] 如下所示:


package spring.security.install;

import generic.jpa.config.ConfigJpa;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@Configuration
@EnableJpaRepositories(basePackages = { "spring.security.repositories" })
@Import({ ConfigJpa.class })
public class AppConfig {
}
  • 第 10 行:指定应用程序中 [repositories] 的位置。它们位于依赖项 [spring-security-server-jpa-generic] 的 [spring.security.repositories] 包中
  • 第 11 行:导入 [ConfigJpa] 类的 Bean,该类用于配置项目的 [JPA] 层。该类位于依赖项 [mysql-config-jpa-hibernate] 中:
  

类 [CreateUsers] 如下所示:


package spring.security.install;

import generic.jpa.entities.dbproduitscategories.Role;
import generic.jpa.entities.dbproduitscategories.User;
import generic.jpa.entities.dbproduitscategories.UserRole;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;

import spring.security.repositories.RoleRepository;
import spring.security.repositories.UserRepository;
import spring.security.repositories.UserRoleRepository;

public class CreateUsers {

    public static void main(String[] args) {

        // 结束
        System.out.println("Travail en cours...");

        // 创建三个用户
        String[] logins = { "admin", "user", "guest" };
        String[] passwds = { "admin", "user", "guest" };
        String[] roles = { "admin", "user", "guest" };

        // Spring 上下文
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        for (int i = 0; i < logins.length; i++) {
            // 获取第 i 号用户的信息
            String login = logins[i];
            String password = passwds[i];
            String roleName = String.format("ROLE_%s", roles[i].toUpperCase());
            // 该角色是否已存在?
            Role role = roleRepository.findRoleByName(roleName);
            // 如果不存在则创建
            if (role == null) {
                role = roleRepository.save(new Role(roleName));
            }
            // 该用户是否已存在?
            User user = userRepository.findUserByLogin(login);
            // 如果不存在,则创建该用户
            if (user == null) {
                // 使用 bcrypt 对密码进行哈希处理
                String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
                // 保存用户
                user = userRepository.save(new User(null, null, login, login, crypt));
                // 建立与角色的关联
                userRoleRepository.save(new UserRole(user, role));
            } else {
                // 用户已存在——是否拥有所请求的角色?
                boolean trouvé = false;
                for (Role r : userRepository.getRoles(user.getId())) {
                    if (r.getName().equals(roleName)) {
                        trouvé = true;
                        break;
                    }
                }
                // 若未找到,则创建与角色的关联
                if (!trouvé) {
                    userRoleRepository.save(new UserRole(user, role));
                }
            }
        }
        // 关闭 Spring 上下文
        context.close();
        // 结束
        System.out.println("Travail terminé...");
    }

}
  • 第 22-24 行:定义了三个用户的登录名、密码和角色;
  • 第 27 行:基于配置类 [AppConfig] 构建 Spring 上下文;
  • 第28-30行:获取三个[Repository]对象的引用,这些引用可用于创建用户;
  • 第31行:创建这三个用户;
  • 第33-35行:创建第i个用户的相关信息;
  • 第 37 行:检查该角色是否已存在;
  • 第39-41行:如果不存在,则在数据库中创建该角色。其名称将采用[ROLE_XX]的格式;
  • 第43行:检查登录名是否已存在;
  • 第 45-52 行:若登录名不存在,则在数据库中创建;
  • 第 47 行:对密码进行加密。此处使用了 Spring Security 的 [BCrypt] 类(第 8 行)。因此需要该框架的依赖包。文件 [pom.xml] 包含以下依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • 第 49 行:将用户信息写入数据库;
  • 第 51 行:以及将其与角色关联的关系;
  • 第 55-60 行:若登录用户已存在——则检查其现有角色中是否已包含要分配的角色;
  • 第 62-64 行:若未找到目标角色,则在表 [USERS_ROLES] 中创建一条记录,将用户与其角色关联;
  • 未对可能出现的异常进行防护。这是一个用于快速创建用户的辅助类;

要运行该项目,需执行名为 [spring-security-create-users-hibernate-eclipselink] 的运行配置:

 

我们刚刚构建了两个安全的 Web 服务:

  • 一个采用[security / web / JDBC / MySQL]架构;
  • 另一个采用 [security / web / Hibernate / MySQL] 架构;

现在我们来探讨另外两种架构:

  • 一种是 [security / web / EclipseLink / SQL Server 2014 Express] 架构;
  • [security / web / OpenJpa / Oracle Express]架构;

  • 在 [1] 中,加载配置 [JDBC / SQL Server] 层和 [JPA / EclipseLink / SQL Server] 层的项目;

注意:按 Alt-F5 键,然后重新生成所有 Maven 项目。

假设 SGBD SQL 服务器已启动,且数据库 [dbproduitscategories] 已生成。首先,我们需要填充该数据库中的 [USERS, ROLES, USERS_ROLES] 表。 为此,请执行名为 [spring-security-create-users-hibernate-eclipselink] 的运行配置:

该配置将向以下三个表中填充数据:

 
 
  • 使用名为 [spring-security-server-jpa-generic-hibernate-eclipselink][1] 的配置启动安全 Web 服务;
  • 使用名为 [spring-security-client-generic-JUnitTestDao][2] 的配置运行测试 JUnitTestDao。该测试应成功通过 [3];

20.5.5. 架构 [security / web / OpenJpa / Oracle Express]

  • 在 [1] 中,加载了配置 [JDBC / Oracle Express] 层和 [JPA / OpenJpa / Oracle Express] 层的项目;

注意:按 Alt-F5 键,然后重新生成所有 Maven 项目。

假设 Oracle Express 实例 SGBD 已启动,且数据库 [dbproduitscategories] 已生成。首先,我们需要填充该数据库中的 [USERS, ROLES, USERS_ROLES] 表。 为此,请执行名为 [spring-security-create-users-openjpa] 的运行配置:

该配置将向以下三个表中填充数据:

 
 
  • 使用名为 [spring-security-server-jpa-generic-openjpa][1-2] 的配置启动安全 Web 服务;
  • 使用名为 [spring-security-client-generic-JUnitTestDao][3] 的配置运行测试 JUnitTestDao。该测试应成功通过 [4];