用户登录验证功能是最基础的功能了,这里顺便就要复习一下Spring Security
的登录验证功能。
我打算使用Spring Security
提供的UserDetailsService
和UserDetails
接口来完成从数据库中读取用户并且进行验证的功能。为此有如下几个步骤:
- 创建用户表,
Role
表和二者的多对多关系表,使用Hibernate
来自行创建表 - 编写
UserDetailsService
和UserDetails
实现类 - 编写自定义登录模板
- 配置
Spring Security
来进行登录验证
为了达成这个目的,还得复习一下Hibernate
与Thymeleaf
的内容。Hibernate
在旧博客里仔细学习过Hibernate
,不过依然有很多地方需要仔细再看看,Thymeleaf
没有系统的学习过,必须开一个支线专门学一下。
创建User
,Role
类和多对多关系
在cc.conyli.fms
下创建entity
包,创建一个User
类,我这里选择直接让其实现UserDetails
接口。
先来逐步编写其中的域,有一个id
,一个enabled
,然后是username
和password
。用户名和密码域的名称设置为username
和password
,和Spring Security
内部的验证功能的键名一致,比较方便。如果域改成其他名称,需要在Spring Security
中设置。
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.validator.constraints.Length;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import java.util.Collection;
@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "id")
private Integer id;
@Column(name= "enabled")
@ColumnDefault("false")
private boolean enabled;
@NotEmpty(message = "用户名不能为空")
@Length(max = 150, message = "用户名长度最多为150个字符")
@Column(name = "username", nullable = false, unique = true, length = 150)
private String username;
@NotEmpty(message = "密码不能为空")
@Column(name = "password", nullable = false, length = 128)
@Length(max = 128, message = "密码最长为128位")
private String password;
}
然后是Role
表,User
和Role
是多对多的关系,方便以后进行添加权限来方便快捷的控制。
import org.hibernate.validator.constraints.Length;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
@Entity
@Table(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "id")
private Integer id;
@NotEmpty(message = "权限名不能为空")
@Length(max = 30, message = "权限最长30个字符")
@Column(name = "role", nullable = false, unique = true, length = 30)
private String role;
public Role() {
}
public String getRole() {
return role;
}
public Role(String role) {
this.role = role;
}
@Override
public String toString() {
return "Role{" +
"id=" + id +
", role='" + role + '\'' +
'}';
}
}
然后是进行多对多映射,这里由于会从User
去查询Role
,所以将多对多设置在User
类中:
public class User implements UserDetails {
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
private List<Role> roles;
}
这里因为要立刻把用户对应的权限查出来,所以就采用了EAGER
,不过本来也不能跨Session
使用。
然后来编写UserDetails
接口中的方法:
public class User implements UserDetails {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
roles.forEach(role -> authorities.add(new SimpleGrantedAuthority(role.getRole())));
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
这样就编写好了两个基础的User
和Role
类,其他的Getter
和Setter
方法都省略,并且做了多对多映射,同时User
类还是UserDetails
的实现类。还要注意的是,对于User
和Role
还要编写equals
方法,判断相等的条件是id
相同。
UserDetailsService
和UserDetails
实现类
User
类同时作为UserDetails
的实现类,不过UserDetailsService
就要自行编写了,这里涉及到需要从数据库中取到用户,然后返回,先看一下UserDetailsService
接口:
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
就一个方法,即通过用户名来获取用户,同时返回UserDetails
类型的对象。创建service
包,然后创建一个AuthUserService
来实现这个接口:
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 javax.transaction.Transactional;
@Service
public class AuthUserService implements UserDetailsService {
@Override
@Transactional
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return null;
}
}
然后就是需要从数据库中读出User
了,这里可以简单用JPA
来实现,创建dao
包,然后在其中创建一个UserDao
接口,继承CrudRepository
:
package cc.conyli.fms.dao;
import cc.conyli.fms.entity.User;
import org.springframework.data.repository.CrudRepository;
public interface UserDao extends CrudRepository<User, Integer> {
User findByUsername(String username);
}
由于JPA
的便利性,只需要定义接口方法为findByUsername
即可,JPA
会自动解析我们想要的查询方式,这里也可以手工编写,待我之后为了实现业务逻辑的时候仔细研读即可。
之后把UserDao
注入到AuthUserService
里:
@Service
public class AuthUserService implements UserDetailsService {
private final UserDao userDao;
@Autowired
public AuthUserService(UserDao userDao) {
this.userDao = userDao;
}
@Override
@Transactional
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return userDao.findByUsername(s);
}
}
这样就完成了UserDetailsService
和UserDetails
实现类
配置Spring Security
为了检测目前为止是否能够生效,可以先来配置一下Spring Security
,看看我们编写的方法是否能够生效:
在cc.conyli.fms
下创建config
包,再创建SecurityConfig
类,然后用这个类来配置Spring Security
:
package cc.conyli.fms.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 注入userDetailsService
private final UserDetailsService userDetailsService;
@Autowired
public SecurityConfig(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
// 创建BCrypt密码编码器的Bean
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 配置用户认证使用自行编写的UserDetailsService实现类=AuthUserService
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
// 配置所有路径都需要验证,使用Spring Security提供的表单
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().permitAll();
}
}
这时候就可以先行启动项目,然后访问任意的路径,会弹出Spring Security
自带的登录页面,之后输入用户名和密码就可以进行登录了。如果用户的enabled
属性是false
,会有提示无法登录。
如果输入了不存在的用户名,会发现提示为:
UserDetailsService returned null, which is an interface contract violation
这个原因是因为在AuthUserService
的loadUserByUsername
中没有抛出异常,所以需要修改一下这个方法:
@Override
@Transactional
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userDao.findByUsername(s);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
} else {
return user;
}
}
这样提示就会变成Bad credentials
,表示用户名或者密码错误。看上去更加合理一些。
接下来就是要编写自定义的登录界面,涉及到Thymeleaf
的使用,以及自行编写控制器。