前言
网络上关于Spring Security实现登录注册的文章很多,但大多存在2个问题,一是讲的不清不楚,看完文章还是没明白怎么使用;二是比较陈旧。本文在前文基础上做了些的改进,尽量讲清楚如何使用。(请注意,本文是入门教程,只讲如何使用,不涉及源码分析)
准备
IDEA
Spring Boot 2.1.0
Spring Security 5.1
Thymeleaf 3.0
JDK 1.8
Maven 3.5.2 IDEA 利用Spring Initializr创建工程spring-security-login-register,添加依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 <?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 > com.geelaro</groupId > <artifactId > register</artifactId > <version > 0.0.1-SNAPSHOT</version > <packaging > jar</packaging > <name > register</name > <description > Demo project for Spring Boot</description > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.1.0.RELEASE</version > <relativePath /> </parent > <properties > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding > <java.version > 1.8</java.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-jpa</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency > <dependency > <groupId > org.thymeleaf.extras</groupId > <artifactId > thymeleaf-extras-springsecurity5</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > org.springframework.security</groupId > <artifactId > spring-security-test</artifactId > <scope > test</scope > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
其他具体配置请在文末看代码。
创建Model
先创建一个User实体,再创建一个Role。
User
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(nullable = false,unique = true) private String name; @Column(nullable = false) private String passWd; @Column(nullable = false,unique = true) private String email; @Column(nullable = false) private Long createTime; @Column(nullable = false) private Long lastModifyTime; @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "users_roles", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")) private List<Role> roles; }
Role
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Entity public class Role { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @ManyToMany(mappedBy = "roles") private Collection<User> users; private String name; public Long getId () { return id; } }
同时还需要再创建一个data transfer object (DTO)表单,用作web和服务层中介。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class UserDto { @NotEmpty @NotNull private String name; @NotEmpty @NotNull @Email private String email; @NotEmpty @NotNull @Size(min = 6,max = 20) private String passWd; @NotEmpty @NotNull @Size(min = 6,max = 20) private String matchPassWd; }
@NotEmpty 限制不为空,@Size限制了密码最短6位,最长20位。
服务层
dao层UserRepository、RoleRepository接口,直接继承JpaRepository.
1 2 3 4 5 6 7 8 9 10 11 12 # User public interface UserRepository extends JpaRepository<User,Long> { User findByNameOrEmail(String name,String email); User findByEmail(String email); User findByName(String name); } # Role public interface RoleRepository extends JpaRepository<Role,Long> { Role findByName(String name); }
UserService 接口
1 2 3 4 5 6 7 8 public interface IUserService { User save(User user); User findByEmail(String email); User findByName(String name); boolean checkUserByName(String name); boolean checkUserByEmail(String email);
UserService接口实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @Service public class UserService implements IUserService { @Autowired private UserRepository userRepository; @Autowired private RoleRepository roleRepository; @Autowired private BCryptPasswordEncoder passwordEncoder; @Transactional @Override public User save(User user) { return userRepository.save(user); } @Override public User findByEmail(String email) { return userRepository.findByEmail(email); } @Override public User findByName(String name) { return userRepository.findByName(name); } @Override public boolean checkUserByName(String name) { User user = findByName(name); return user != null; } @Override public boolean checkUserByEmail(String email) { User user = findByEmail(email); return user != null; } /** * 创建一个新的user,角色为ROLE_USER * @param userDto * @return */ public User registerNewAccount(UserDto userDto) { User user = new User(); user.setEmail(userDto.getEmail()); user.setName(userDto.getName()); user.setPassWd(passwordEncoder.encode(userDto.getPassWd())); user.setRoles(Arrays.asList(roleRepository.findByName("ROLE_USER"))); user.setCreateTime(new Date().getTime()); user.setLastModifyTime(new Date().getTime()); return save(user); } }
Web层
写了4个页面,首页”/“,注册”/register”、登录”/login”,以及登录后的用户信息详情页”/userInfo”。把他们分别放到两个controller中,method为get的放到IndexController,需要数据处理的放在UserController中。
IndexController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Controller public class IndexController { @GetMapping("/") public String index(){ return "index"; } @GetMapping("/register") public String createUser(Model model){ model.addAttribute("user",new UserDto()); return "register"; } @GetMapping("/login") public String login(Model model){ model.addAttribute("user",new UserDto()); return "login"; } @GetMapping("/login?error") public String loginError(Model model){ model.addAttribute("loginError",true); return "login"; }
UserController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 @Controller public class UserController { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); @Autowired private UserService userService; @PostMapping("/register") public String createUser(@ModelAttribute("user") @Valid UserDto userDto, BindingResult result, Model model) { if (result.hasErrors()) { return "register"; } //check name 是否已使用 if (userService.checkUserByName(userDto.getName())) { result.rejectValue("name", "error.user", "name已使用"); logger.info("name已存在!"); return "register"; } //check email 是否已注册。 if (userService.checkUserByEmail(userDto.getEmail())) { result.rejectValue("email", "error.user", "Email已注册"); logger.info("email已注册!"); return "register"; } //check password equal if (!checkPassWordUniform(userDto.getPassWd(),userDto.getMatchPassWd())){ result.rejectValue("passWd","error.user","两次输入密码不一致"); return "register"; } try { createUserAccount(userDto); } catch (DataIntegrityViolationException e) { result.rejectValue("email", "error.user","Email already exists."); result.rejectValue("name", "error.user","Name already exists"); } return "redirect:/userInfo"; } @GetMapping("/userInfo") public String userInfo(Model model) { UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); String name = userDetails.getUsername(); String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); model.addAttribute("name", name); model.addAttribute("date", date); return "userInfo"; } private User createUserAccount(UserDto userDto) { User registered = null; registered = userService.registerNewAccount(userDto); return registered; } private boolean checkPassWordUniform(String passWd,String matchPassWd){ return passWd.equals(matchPassWd); }
注册时,使用@Valid注解启用表单验证,因为整体比较简单,自定义验证我也写在了controller中。最好的是另外写一个类实现Validator接口,在类中自定义表单验证。
这样注册功能就已经完成了,接下来来看登录。
认证配置
Spring Security是自带登录逻辑的,我们只需要配置和自定义登录界面,其他的交给Security处理。新建一个WebSecurityConfig 继承 WebSecurityConfigurerAdapter类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private DataSource dataSource; @Autowired private CustomUserDetailService userDetailService; /** * 密码加密 * @return */ @Bean public BCryptPasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } /** * auth的方式 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() //csrf不可用 .authorizeRequests() .antMatchers("/static/**","/css/**").permitAll() //访问允许静态文件 .antMatchers("/","/register").permitAll() //允许访问首页和注册页 .anyRequest().authenticated() .and() .formLogin().loginPage("/login").failureUrl("/login?error")//指定登录页和登录失败页 .defaultSuccessUrl("/userInfo") //登录成功跳转页 .usernameParameter("name") .passwordParameter("passWd") .and() .logout().logoutSuccessUrl("/login").permitAll() //退出登录跳转页 .and() .rememberMe() //remember me .tokenRepository(tokenRepository()) //存储 .userDetailsService(userDetailService) .tokenValiditySeconds(24*60*60);//token有效期24h } public PersistentTokenRepository tokenRepository(){ JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl(); db.setDataSource(this.dataSource); return db; }
这个配置比较繁琐,我在比较难缠的地方加了注解。Spring Security要求如果要自定义账号密码,需要实现自带的UserDetailService接口,用户验证当前用户。其中CustomUserDetailService就是我们实现的类,而UserDetails 是 Spring 用来记录当前用户,就是用户登录完之后,系统里保存就是当前这个用户的 UserDetails,所以可以在不同的用户端显示不同的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Service public class CustomUserDetailService implements UserDetailsService { private final static Logger logger = LogManager.getLogger(CustomUserDetailService.class); @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException { User user = userRepository.findByName(name); if (user==null){ logger.error("Not Found name "+ name); throw new UsernameNotFoundException("Not Found name "+ name); } return new org.springframework.security.core.userdetails.User( user.getName(), user.getPassWd(), getAuthorities(user.getRoles())); } private static List<GrantedAuthority> getAuthorities (List<Role> roles) { List<GrantedAuthority> authorities = new ArrayList<>(); for (Role role:roles){ authorities.add(new SimpleGrantedAuthority(role.getName())); } return authorities; } }
视图层
获取当前用户有2个方法:
代码层
1 2 UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); String name = userDetails.getUsername();
HTML
1 2 3 4 5 6 <div> <p>登录的用户名 : <strong class="reg" sec:authentication="name"></strong> </p> 登录成功的用户角色 : <em class="reg" sec:authentication="principal.authorities"></em> </div>
验证
首先在表Role插入两条数据,”ROLE_USER”和”ROLE_ADMIN”,然后启动程序。
未登录首页
登录后首页
注册一个user
表单验证成功。
登录的用户详情页
Remember me
Spring Security 有remember me功能,使用十分简单。
前端login.html中添加 `
`,后端WebSecurityConfig添加一个存储方法即可。
1 2 3 4 5 6 7 8 9 public PersistentTokenRepository tokenRepository(){ //存储内存,不推荐 // InMemoryTokenRepositoryImpl memory =new InMemoryTokenRepositoryImpl(); // return memory; /** 存档到数据库中 **/ JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl(); db.setDataSource(this.dataSource); return db; }
选择存储到数据库时,需要手动建表”persistent_logins”,建表sql:
1 2 3 4 5 6 7 8 9 DROP TABLE IF EXISTS `persistent_logins`; CREATE TABLE `persistent_logins` ( `username` varchar(64) NOT NULL, `series` varchar(64) NOT NULL, `token` varchar(64) NOT NULL, `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`series`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; SET FOREIGN_KEY_CHECKS=1;
踩过的坑
login中thymeleaf的坑
如果login.html中form表单input 标签中使用的th:field="*{}",那么在WebSecurityConfig一定要添加这"usernameParameter"和"passwordParameter",不然登录后报错"name not found".
1 2 3 4 5 6 7 8 @Override protected void configure(HttpSecurity http) throws Exception { http ........... .formLogin().loginPage("/login").failureUrl("/login?error") .defaultSuccessUrl("/userInfo") .usernameParameter("name") .passwordParameter("passWd")
如果使用的是<input type=password name ="password />
,则配置类中不需要添加这两个。
thymeleaf-extras-springsecurity
Spring Boot 2.0默认使用的Spring Security版本是5.x,根据官网[thymeleaf-extras-springsecurity](https://github.com/thymeleaf/thymeleaf-extras-springsecurity)
thymeleaf-extras-springsecurity3 for integration with Spring Security 3.x thymeleaf-extras-springsecurity4 for integration with Spring Security 4.x thymeleaf-extras-springsecurity5 for integration with Spring Security 5.x
选择的thymeleaf-extras-springsecurity版本应是:
1 2 3 4 <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency>
代码及参考文章
Spring-security-login-register Spring Boot Security下的注册登录