在数据库中存储密文密码
很显然,实际开发中绝对不能使用明文密码,否则数据泄露的可能性非常大。
Spring Security针对密码推荐bcrypt算法,bcrypt算法可以一次性计算好hash后的值,自动加随机的盐,可以防止暴力破解。是一个使用广泛的算法。
密码学在计算机科学出现之前就有了,而哈希算法也是一个很大的主体,这里不展开,只学习如何使用。
当我们有一个明文密码的时候,我们可以通过第三方工具或者一些网站生成bcrypt之后的密码,也可以编写Java代码来加密,这里要学习如何使用代码来进行加密。
现在我们的需求就是用户输入明文密码,然后我们在数据库中保存加密后的密文,这样即使开发者泄密,也很难知道是原来密码是什么。开发的步骤如下:
- 由于bcrypt之后的密文长度较长,创建一个新的数据库和表,其中密文字段需要有68字符长,因为密文有60个字符,而之前的
{bcrypt}
是8个字符。
- 修改数据库配置,指向新的表
先来创建新的表,大部分和上次创建表一样:
DROP DATABASE IF EXISTS `spring_security_demo_bcrypt`;
CREATE DATABASE IF NOT EXISTS `spring_security_demo_bcrypt`;
USE `spring_security_demo_bcrypt`;
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`username` varchar(50) NOT NULL,
`password` char(68) NOT NULL,
`enabled` tinyint(1) NOT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `users`
VALUES
('john','{bcrypt}$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K',1),
('mary','{bcrypt}$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K',1),
('susan','{bcrypt}$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K',1);
DROP TABLE IF EXISTS `authorities`;
CREATE TABLE `authorities` (
`username` varchar(50) NOT NULL,
`authority` varchar(50) NOT NULL,
UNIQUE KEY `authorities_idx_1` (`username`,`authority`),
CONSTRAINT `authorities_ibfk_1` FOREIGN KEY (`username`) REFERENCES `users` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `authorities`
VALUES
('john','ROLE_EMPLOYEE'),
('mary','ROLE_EMPLOYEE'),
('mary','ROLE_MANAGER'),
('susan','ROLE_EMPLOYEE'),
('susan','ROLE_ADMIN');
这里可以看到,用户密码写进去的数据是由{bcrypt}开头,代表之后是一个bcrypt加密的密文。Spring Security就会使用bcrypt算法来处理密文。
这里的密文对应的明文密码是fun123
然后修改数据库配置文件指向新的数据表:
jdbc.url=jdbc:mysql://localhost:3306/spring_security_demo_bcrypt?useSSL=false
来运行一下项目看看,密码变成了fun123。就完成了数据库的配置。
用户注册
用户身份验证之后的一个主要问题就是用户注册了,这样就把用户相关的功能做成了一个内容管理系统,让用户自己去完成注册和登录的功能。
我们来完成一个用户注册功能,用户注册核心功能就是提供一个表单,用户填写该表单后将对应数据写入数据库,用户登录的时候,就在数据库中查询用户信息,这里我们要使用Hibernate来操作数据库。由于步骤比较多,一步一步来操作
创建用户和角色表
由于用户表和原来的用户表不一样,这次创建一个新的数据库和数据表:
DROP DATABASE IF EXISTS `spring_security_custom_user_demo`;
CREATE DATABASE IF NOT EXISTS `spring_security_custom_user_demo`;
USE `spring_security_custom_user_demo`;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` char(80) NOT NULL,
`first_name` varchar(50) NOT NULL,
`last_name` varchar(50) NOT NULL,
`email` varchar(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
INSERT INTO `user` (username,password,first_name,last_name,email)
VALUES
('john','$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K','John','Doe','john@luv2code.com'),
('mary','$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K','Mary','Public','mary@luv2code.com'),
('susan','$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K','Susan','Adams','susan@luv2code.com');
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
INSERT INTO `role` (name)
VALUES
('ROLE_EMPLOYEE'),('ROLE_MANAGER'),('ROLE_ADMIN');
DROP TABLE IF EXISTS `users_roles`;
CREATE TABLE `users_roles` (
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`user_id`,`role_id`),
KEY `FK_ROLE_idx` (`role_id`),
CONSTRAINT `FK_USER_05` FOREIGN KEY (`user_id`)
REFERENCES `user` (`id`)
ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT `FK_ROLE` FOREIGN KEY (`role_id`)
REFERENCES `role` (`id`)
ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
SET FOREIGN_KEY_CHECKS = 1;
INSERT INTO `users_roles` (user_id,role_id)
VALUES
(1, 1),
(2, 1),
(2, 2),
(3, 1),
(3, 3)
这里我们采用了更贴近实际开发的数据表:用户表是根据实际情况所需要的字段建立的,然后用户表和角色表是多对多的关系,一个用户可以有多个角色,一个角色下边有多个用户。
配置所有的Maven依赖
这次要使用Hibernate和相关的验证器,需要配置一系列依赖:
<!--Spring TX-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${springframework.version}</version>
</dependency>
<!-- Spring ORM -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${springframework.version}</version>
</dependency>
<!-- Hibernate Core -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- Hibernate Validator -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.7.Final</version>
</dependency>
配置密码生成器
可以发现在数据库中保存的密文密码不是以{bcrypt}
开头的,这是因为现代开发中,一般要通过密码生成器生成密文然后写入数据库。
所以需要在Spring Security的配置类中创建Bean用于生成密文和解密。
//初始化一个bcrypt编码Bean
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//创建认证提供器
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider auth = new DaoAuthenticationProvider();
auth.setUserDetailsService(userService);
auth.setPasswordEncoder(passwordEncoder());
return auth;
}
第一个是创建了一个Spring Security内置的bcrypt的编码器,然后创建了一个认证器,将编码器和用户服务提供给这个认证器,这个认证器就可以拿着用户数据和密码去进行验证。
这里userService
还没有编写,会在之后进行编写。
创建表单数据对象和验证器
这个是用于表单数据的对象,还不是直接对应数据库中的User和Role的对象,所以不放在entity目录中,创建一个user目录,然后在其中创建CrmUser.java作为用户数据对象:
package cc.conyli.entity;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class CrmUser {
@NotNull(message = "is required")
@Size(min = 1, message = "is required")
private String userName;
@NotNull(message = "is required")
@Size(min = 1, message = "is required")
private String password;
@NotNull(message = "is required")
@Size(min = 1, message = "is required")
private String matchingPassword;
@NotNull(message = "is required")
@Size(min = 1, message = "is required")
private String firstName;
@NotNull(message = "is required")
@Size(min = 1, message = "is required")
private String lastName;
@NotNull(message = "is required")
@Size(min = 1, message = "is required")
private String email;
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getMatchingPassword() {
return matchingPassword;
}
public void setMatchingPassword(String matchingPassword) {
this.matchingPassword = matchingPassword;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public CrmUser() {
}
public CrmUser(String userName, String password, String matchingPassword, String firstName) {
this.userName = userName;
this.password = password;
this.matchingPassword = matchingPassword;
this.firstName = firstName;
}
@Override
public String toString() {
return "CrmUser{" +
"userName='" + userName + '\'' +
", password='" + password + '\'' +
", matchingPassword='" + matchingPassword + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
'}';
}
}
此时还不能直接使用,由于@ValidEmail
还没有编写,而且我们给这个类添加了一个不属于数据库里的matchingPassword字段,用来验证用户两次密码填写是否一致。所以还需要编写自定义的验证器。
在conyli.cc下边创建validation包,然后在其中编写验证器注解类和对应的验证器类,一共是两个字段,所以有四个类。
package cc.conyli.validation;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class EmailValidator implements ConstraintValidator<ValidEmail, String> {
private Pattern pattern;
private Matcher matcher;
private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
+ "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
@Override
public boolean isValid(final String email, final ConstraintValidatorContext context) {
pattern = Pattern.compile(EMAIL_PATTERN);
if (email == null) {
return false;
}
matcher = pattern.matcher(email);
return matcher.matches();
}
@Override
public void initialize(ValidEmail validEmail) {
}
}
package cc.conyli.validation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Constraint(validatedBy = EmailValidator.class)
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidEmail {
String message() default "Invalid email";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
package cc.conyli.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.springframework.beans.BeanWrapperImpl;
public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {
private String firstFieldName;
private String secondFieldName;
private String message;
@Override
public void initialize(final FieldMatch constraintAnnotation) {
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
message = constraintAnnotation.message();
}
@Override
public boolean isValid(final Object value, final ConstraintValidatorContext context) {
boolean valid = true;
try
{
final Object firstObj = new BeanWrapperImpl(value).getPropertyValue(firstFieldName);
final Object secondObj = new BeanWrapperImpl(value).getPropertyValue(secondFieldName);
valid = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
}
catch (final Exception ignore)
{
// we can ignore
}
if (!valid){
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(firstFieldName)
.addConstraintViolation()
.disableDefaultConstraintViolation();
}
return valid;
}
}
package cc.conyli.validation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Constraint(validatedBy = FieldMatchValidator.class)
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FieldMatch {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String first();
String second();
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List
{
FieldMatch[] value();
}
}
然后需要将注解添加到CrmUser类上,首先是email字段:
@ValidEmail
@NotNull(message = "is required")
@Size(min = 1, message = "is required")
private String email;
然后是一个验证两个字段的验证器,需要添加到类上:
@FieldMatch.List({
@FieldMatch(first = "password",second = "matchingPassword", message = "The password fields must match")
})
public class CrmUser {
......
}
这个是将验证器里的两个属性设置为password属性和matchingPassword属性,然后获取两个属性比较是否相同,如果不同就返回错误信息。
这里的自定义验证器要比课程里学的硬核一些,不过看一下代码就可以知道做了什么事情,还是属于比较简单的逻辑。
配置数据库连接属性
这一步很简单,修改一下配置为新创建的数据库,还需要把Hibernate的配置也加进去:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_security_custom_user_demo?useSSL=false
jdbc.user=springstudent
jdbc.password=springstudent
connection.pool.initialPoolSize=5
connection.pool.minPoolSize=5
connection.pool.maxPoolSize=20
connection.pool.maxIdleTime=3000
hibernate.dialect=org.hibernate.dialect.MySQLDialect
hibernate.show_sql=true
hiberante.packagesToScan=cc.conyli.entity
编写从数据库中取出用户数据的User类和Role类
因为后边要用Hibernate操作数据库,之前编写的CrmUser类其实是用来注册表单的时候生成的数据对象,不是直接对应数据库中表的类。所以要再编写一下User和Role类,主要关注多对多关系:
package cc.conyli.entity;
import javax.persistence.*;
import java.util.Collection;
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "username")
private String userName;
@Column(name = "password")
private String password;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@Column(name = "email")
private String email;
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(name = "users_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Collection<Role> roles;
public User() {
}
public User(String userName, String password, String firstName, String lastName, String email) {
this.userName = userName;
this.password = password;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public User(String userName, String password, String firstName, String lastName, String email,
Collection<Role> roles) {
this.userName = userName;
this.password = password;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.roles = roles;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Collection<Role> getRoles() {
return roles;
}
public void setRoles(Collection<Role> roles) {
this.roles = roles;
}
@Override
public String toString() {
return "User{" + "id=" + id + ", userName='" + userName + '\'' + ", password='" + "*********" + '\''
+ ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", email='" + email + '\''
+ ", roles=" + roles + '}';
}
}
package cc.conyli.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
public Role() {
}
public Role(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Role{" + "id=" + id + ", name='" + name + '\'' + '}';
}
}
由于数据验证的内容主要交给了对应注册功能的表单,这里就没有配置不必要的验证器了。
创建表单页面
给登录页面添加一个Register按钮如下:
<div>
<a href="${pageContext.request.contextPath}/register/showRegistrationForm" class="btn btn-primary" role="button" aria-pressed="true">Register New User</a>
</div>
很显然一会要来编写控制器。
先继续在WEB-INF/view/目录下创建一个用户登录表单页面registration-form.jsp:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="zh-cn">
<head>
<title>Register New User Form</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Reference Bootstrap files -->
<link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<style>
.error {
color: red
}
</style>
</head>
<body>
<div>
<div id="loginbox" style="margin-top: 50px;"
class="mainbox col-md-3 col-md-offset-2 col-sm-6 col-sm-offset-2">
<div class="panel panel-primary">
<div class="panel-heading">
<div class="panel-title">Register New User</div>
</div>
<div style="padding-top: 30px" class="panel-body">
<!-- Registration Form -->
<form:form action="${pageContext.request.contextPath}/register/processRegistrationForm"
modelAttribute="crmUser"
class="form-horizontal">
<!-- Place for messages: error, alert etc ... -->
<div class="form-group">
<div class="col-xs-15">
<div>
<!-- Check for registration error -->
<c:if test="${registrationError != null}">
<div class="alert alert-danger col-xs-offset-1 col-xs-10">
${registrationError}
</div>
</c:if>
</div>
</div>
</div>
<!-- User name -->
<div style="margin-bottom: 25px" class="input-group">
<span class="input-group-addon"><i class="glyphicon glyphicon-user"></i></span>
<form:errors path="userName" cssClass="error"/>
<form:input path="userName" placeholder="username (*)" class="form-control"/>
</div>
<!-- Password -->
<div style="margin-bottom: 25px" class="input-group">
<span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i></span>
<form:errors path="password" cssClass="error"/>
<form:password path="password" placeholder="password (*)" class="form-control"/>
</div>
<!-- Confirm Password -->
<div style="margin-bottom: 25px" class="input-group">
<span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i></span>
<form:errors path="matchingPassword" cssClass="error"/>
<form:password path="matchingPassword" placeholder="confirm password (*)" class="form-control"/>
</div>
<!-- First name -->
<div style="margin-bottom: 25px" class="input-group">
<span class="input-group-addon"><i class="glyphicon glyphicon-user"></i></span>
<form:errors path="firstName" cssClass="error"/>
<form:input path="firstName" placeholder="first name (*)" class="form-control"/>
</div>
<!-- Last name -->
<div style="margin-bottom: 25px" class="input-group">
<span class="input-group-addon"><i class="glyphicon glyphicon-user"></i></span>
<form:errors path="lastName" cssClass="error"/>
<form:input path="lastName" placeholder="last name (*)" class="form-control"/>
</div>
<!-- Email -->
<div style="margin-bottom: 25px" class="input-group">
<span class="input-group-addon"><i class="glyphicon glyphicon-user"></i></span>
<form:errors path="email" cssClass="error"/>
<form:input path="email" placeholder="email (*)" class="form-control"/>
</div>
<!-- Register Button -->
<div style="margin-top: 10px" class="form-group">
<div class="col-sm-6 controls">
<button type="submit" class="btn btn-primary">Register</button>
</div>
</div>
</form:form>
</div>
</div>
</div>
</div>
</body>
</html>
里边的主要关注点是表单标签绑定的model中的对象名是crmUser
,这是我们要通过控制器传递给JSP页面的属性。完成了上边的准备工作,后边要依次创建控制器-service-Dao层了。
创建控制器
表单控制器控制两个链接:
- /register/showRegistrationForm
- /register/processRegistrationForm
很显然一个展示表单,一个处理表单。
package cc.conyli.controller;
import java.util.logging.Logger;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import cc.conyli.user.CrmUser;
import cc.conyli.entity.User;
import cc.conyli.service.UserService;
@Controller
@RequestMapping("/register")
public class RegistrationController {
//注入业务层的userService对象,使用这个对象通过用户名找到用户数据然后创建user对象
@Autowired
private UserService userService;
private Logger logger = Logger.getLogger(getClass().getName());
//这是以前用过的初始化处理,这里使用了去掉两端空白的预先处理
@InitBinder
public void initBinder(WebDataBinder dataBinder) {
StringTrimmerEditor stringTrimmerEditor = new StringTrimmerEditor(true);
dataBinder.registerCustomEditor(String.class, stringTrimmerEditor);
}
//展示表单
@GetMapping("/showRegistrationForm")
public String showMyLoginPage(Model theModel) {
theModel.addAttribute("crmUser", new CrmUser());
return "registration-form";
}
//处理表单数据,如果验证通过,就保存到数据库中
@PostMapping("/processRegistrationForm")
public String processRegistrationForm(
@Valid @ModelAttribute("crmUser") CrmUser theCrmUser,
BindingResult theBindingResult,
Model theModel) {
String userName = theCrmUser.getUserName();
logger.info("Processing registration form for: " + userName);
// 验证表单
if (theBindingResult.hasErrors()){
return "registration-form";
}
// 检查是否用户已经存在
User existing = userService.findByUserName(userName);
if (existing != null){
theModel.addAttribute("crmUser", new CrmUser());
theModel.addAttribute("registrationError", "User name already exists.");
logger.warning("User name already exists.");
return "registration-form";
}
// 如果通过验证,保存入数据库
userService.save(theCrmUser);
logger.info("Successfully created user: " + userName);
return "registration-confirmation";
}
}
控制器里边注入的Service层的userService还未编写,还有处理表单后返回的registration-confirmation.jsp也未编写,按照开发逻辑会逐步编写。
创建Service层
这里还是遵循开发原则,先创建接口再创建类。UserService接口主要有两个方法:
- userService.findByUserName(userName),用来使用用户名获取用户对象
- userService.save(theCrmUser),保存验证后的表单数据到数据库
package cc.conyli.service;
import cc.conyli.entity.User;
import cc.conyli.user.CrmUser;
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UserService extends UserDetailsService {
User findByUserName(String userName);
void save(CrmUser crmUser);
}
然后来创建实现类:
package cc.conyli.service;
import cc.conyli.dao.RoleDao;
import cc.conyli.dao.UserDao;
import cc.conyli.entity.Role;
import cc.conyli.entity.User;
import cc.conyli.user.CrmUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
@Service
public class UserServiceImpl implements UserService {
// need to inject user dao
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
@Transactional
public User findByUserName(String userName) {
// check the database if the user already exists
return userDao.findByUserName(userName);
}
@Override
@Transactional
public void save(CrmUser crmUser) {
User user = new User();
// assign user details to the user object
user.setUserName(crmUser.getUserName());
user.setPassword(passwordEncoder.encode(crmUser.getPassword()));
user.setFirstName(crmUser.getFirstName());
user.setLastName(crmUser.getLastName());
user.setEmail(crmUser.getEmail());
// give user default role of "employee"
user.setRoles(Arrays.asList(roleDao.findRoleByName("ROLE_EMPLOYEE")));
// save user in the database
userDao.save(user);
}
@Override
@Transactional
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
User user = userDao.findByUserName(userName);
if (user == null) {
throw new UsernameNotFoundException("Invalid username or password.");
}
return new org.springframework.security.core.userdetails.User(user.getUserName(), user.getPassword(),
mapRolesToAuthorities(user.getRoles()));
}
private Collection<? extends GrantedAuthority> mapRolesToAuthorities(Collection<Role> roles) {
return roles.stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
}
}
其中用到的Dao对象还未编写。Service层这里的一个方法是根据用户名来查询用户,还一个save方法是将表单数据CrmUser对象的各个属性设置到一个新的User对象上,其中密码字段从明文转换成密文,还会去获取role表中的EMPLOYEE对象,设置与这个用户的关联关系,也就是给用户一个默认的EMPLOYEE身份。
上述的业务逻辑处理完毕之后,再将用户保存到数据库中。
创建DAO层
一样还是通过接口和实现类的方式来编写,由于有两个数据表,将其分离为两个Dao接口和对应实现类。
两个接口分别如下:
package cc.conyli.dao;
import cc.conyli.entity.Role;
public interface RoleDao {
public Role findRoleByName(String theRoleName);
}
package cc.conyli.dao;
import cc.conyli.entity.User;
public interface UserDao {
User findByUserName(String userName);
void save(User user);
}
两个实现类如下:
package cc.conyli.dao;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.query.Query;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import cc.conyli.entity.Role;
@Repository
public class RoleDaoImpl implements RoleDao {
// need to inject the session factory
@Autowired
private SessionFactory sessionFactory;
@Override
public Role findRoleByName(String theRoleName) {
// get the current hibernate session
Session currentSession = sessionFactory.getCurrentSession();
// now retrieve/read from database using name
Query<Role> theQuery = currentSession.createQuery("from Role where name=:roleName", Role.class);
theQuery.setParameter("roleName", theRoleName);
Role theRole = null;
try {
theRole = theQuery.getSingleResult();
} catch (Exception e) {
theRole = null;
}
return theRole;
}
}
package cc.conyli.dao;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.query.Query;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import cc.conyli.entity.User;
@Repository
public class UserDaoImpl implements UserDao {
// need to inject the session factory
@Autowired
private SessionFactory sessionFactory;
@Override
public User findByUserName(String theUserName) {
// get the current hibernate session
Session currentSession = sessionFactory.getCurrentSession();
// now retrieve/read from database using username
Query<User> theQuery = currentSession.createQuery("from User where userName=:uName", User.class);
theQuery.setParameter("uName", theUserName);
User theUser = null;
try {
theUser = theQuery.getSingleResult();
} catch (Exception e) {
theUser = null;
}
return theUser;
}
@Override
public void save(User theUser) {
// get current hibernate session
Session currentSession = sessionFactory.getCurrentSession();
// create the user ... finally LOL
currentSession.saveOrUpdate(theUser);
}
}
还没有创建sessionFactory的这个Bean,这个会到最后一起在配置文件里创建Bean。
创建注册成功页面
还需要在注册成功的时候创建注册成功页面,做一个很简单的页面:
<html>
<head>
<title>Registration Confirmation</title>
</head>
<body>
<h2>User registered successfully!</h2>
<hr>
<a href="${pageContext.request.contextPath}/showMyLoginPage">Login with new user</a>
</body>
</html>
配置Spring 与Spring Security
这里先需要在config包下创建一个类,用来配置处理成功之后的处理:
package cc.conyli.config;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import cc.conyli.entity.User;
import cc.conyli.service.UserService;
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private UserService userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
System.out.println("\n\nIn customAuthenticationSuccessHandler\n\n");
String userName = authentication.getName();
System.out.println("userName=" + userName);
User theUser = userService.findByUserName(userName);
//把用户数据放到Session里
HttpSession session = request.getSession();
session.setAttribute("user", theUser);
//重定向到根目录
response.sendRedirect(request.getContextPath() + "/");
}
}
这个类的一个关键作用就是把user对象设置到session上,Spring Security内部也是这个机制,将user对象设置到了session上之后,后边的DaoAuthenticationProvider
使用userService
这一系列验证才能生效。
然后是Spring Security的配置类,在最开始我们配置了一个BCryptPasswordEncoder
和一个DaoAuthenticationProvider
两个Bean,现在Service对象有了,继续修改配置,最终的配置文件如下:
package cc.conyli.config;
import cc.conyli.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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 javax.sql.DataSource;
@Configuration
@EnableWebSecurity
public class DemoSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").hasRole("EMPLOYEE")
.antMatchers("/leader/**").hasRole("MANAGER")
.antMatchers("/system/**").hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/showMyLoginPage")
.loginProcessingUrl("/authenticateTheUser")
.successHandler(customAuthenticationSuccessHandler)
.permitAll()
.and()
.logout().permitAll()
.and()
.exceptionHandling().accessDeniedPage("/access-denied/");
}
//初始化一个bcrypt编码Bean
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//创建认证提供器
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider auth = new DaoAuthenticationProvider();
auth.setUserDetailsService(userService);
auth.setPasswordEncoder(passwordEncoder());
return auth;
}
}
这里新的内容主要是新注入的UserService
对象和CustomAuthenticationSuccessHandler
对象,以及在配置方法里,给auth直接设置了一个验证提供类,而不是使用原来硬编码或者是数据库连接池。这样在验证的时候,Spring Security也是通过Hibernate去数据库读取数据。
最后是Spring IOC容器里的Hibernate类配置了,这个已经很熟悉了,完整的配置类如下:
package cc.conyli.config;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import javax.sql.DataSource;
import java.beans.PropertyVetoException;
import java.util.Properties;
import java.util.logging.Logger;
@Configuration
@EnableWebMvc
@EnableTransactionManagement
@ComponentScan(basePackages = "cc.conyli")
@PropertySource("classpath:persistence-mysql.properties")
public class DemoAppConfig {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/view/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
@Autowired
private Environment environment;
private Logger logger = Logger.getLogger(getClass().getName());
//连接池的Bean
@Bean
public DataSource securityDataSource() {
//利用C3PO创建连接池
ComboPooledDataSource securityDataSource = new ComboPooledDataSource();
//设置连接池对象的数据库属性
try {
securityDataSource.setDriverClass(environment.getProperty("jdbc.driver"));
securityDataSource.setJdbcUrl(environment.getProperty("jdbc.url"));
securityDataSource.setUser(environment.getProperty("jdbc.user"));
securityDataSource.setPassword(environment.getProperty("jdbc.password"));
logger.info(">>>> jdbc.url=" + environment.getProperty("jdbc.url"));
logger.info(">>>> jdbc.user=" + environment.getProperty("jdbc.user"));
} catch (PropertyVetoException ex) {
throw new RuntimeException(ex);
}
//设置连接池的连接属性
securityDataSource.setInitialPoolSize(Integer.parseInt(environment.getProperty("connection.pool.initialPoolSize")));
securityDataSource.setMinPoolSize(Integer.parseInt(environment.getProperty("connection.pool.minPoolSize")));
securityDataSource.setMaxPoolSize(Integer.parseInt(environment.getProperty("connection.pool.maxPoolSize")));
securityDataSource.setMaxIdleTime(Integer.parseInt(environment.getProperty("connection.pool.maxIdleTime")));
return securityDataSource;
}
//这个是自己写的类方法,用于获取Hibernate的配置属性
private Properties getHibernateProperties() {
// set hibernate properties
Properties props = new Properties();
props.setProperty("hibernate.dialect", environment.getProperty("hibernate.dialect"));
props.setProperty("hibernate.show_sql", environment.getProperty("hibernate.show_sql"));
return props;
}
//创建SessionFactory的Bean
@Bean
public LocalSessionFactoryBean sessionFactory(){
// create session factorys
LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
// set the properties
sessionFactory.setDataSource(securityDataSource());
sessionFactory.setPackagesToScan(environment.getProperty("hiberante.packagesToScan"));
sessionFactory.setHibernateProperties(getHibernateProperties());
return sessionFactory;
}
//自动事务管理
@Bean
@Autowired
public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) {
// setup transaction manager based on session factory
HibernateTransactionManager txManager = new HibernateTransactionManager();
txManager.setSessionFactory(sessionFactory);
return txManager;
}
}
由于Hibernate也需要连接池Bean,所以只需要配置Hibernate的Bean即可,这里没有使用Spring的Hibernate配置类,而是使用了Hibernate提供的类,但是内容也是一样的,设置好连接池,从文件里读出Entity扫描位置和其他属性然后设置好就可以了。
最后一个Bean之前没接触过,应该是指的自动的事务处理。
运行项目
一开始直接运行的时候,报错,error activating bean validation integration
,这是因为之前我自己在pom.xml中配置了:
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
</dependency>
这个其实是不需要的,有了这个之后会导入另外一个验证器,造成冲突。一般配置servlet相关的API只需要下边这两个依赖:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
</dependency>
之后可以正常启动了,功能测试也都正常。这里有一点要注意的是,DAO类里操作的数据库底层是Hibernate的SessionFactory对象,而且由于用户名是独特的,我们没有直接通过id取用户名。这里是创建了SQL语句去执行,但是这里的SQL语句实际上是HQL语句,即用Entity Class名称替代表名,属性名替代列名,而:uName
是特殊写法,标示一个变量,要在随后使用theQuery.setParameter("uName", theUserName);
去设置,这里和标准的SQL语句有点区别,要特别注意。