SpringBoot2.x整合Security5(完美解决 There is no PasswordEncoder mapped for the id "null")

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SWPU_Lipan/article/details/80586054

问题描述

以前做过一次SpringBoot整合Security的项目,但最近重新尝试SpringBoot整合Security的项目时却碰到了问题

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

原来SpringBoot升级到了2.0之后的版本,Security也由原来的版本4升级到了5,所以花了点时间研究,以此记录

工具

  1. IDEA
  2. Maven

SpringBoot 2.0 整合 Security5

1. 项目主要依赖

security

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

SpringBoot

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

同时引入SpringData JPA持久层框架和 Lombok工具简化GET、SET方法

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

2. SpringBoot配置

这里我使用更加简洁的yml文件来配置

server:
  port: 8888
spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/three_point
  jpa:
    hibernate:
      ddl-auto: update
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    open-in-view: false

ddl-auto: update 使用了JPA的自动建表

3. 实体类

用户类

package com.pan.entity;

import lombok.Data;

import javax.persistence.*;
import java.util.List;

@Entity
@Table
@Data
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String username;
    private String password;
    @ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)
    private List<Role> roles;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
}

角色类

package com.pan.entity;

import lombok.Data;

import javax.persistence.*;

@Entity
@Table
@Data
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
}

UserDetailsService 实现类

package com.pan.service.impl;

import com.pan.entity.Role;
import com.pan.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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 java.util.ArrayList;
import java.util.List;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        com.pan.entity.User user = userRepository.findByUsername(s);
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : user.getRoles()) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return new User(user.getUsername(), user.getPassword(), authorities);
    }
}

这里我使用的是User不继承UserDetails这个接口,而将添加认证用户的操作放在了上面这个类中,网上还有另外一种实现方式


:这里为另外一个项目,属性会有所不同,仅供演示,代码如下:
用户类

@Entity
@Table(name = "user")
@Getter
@Setter
public class Member implements UserDetails {

    @Id
    @GeneratedValue
    private Integer id;

    private String username;

    private String password;

    @ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)
    private List<Role> roles;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> auths = new ArrayList<>();
        List<Role> roles = this.getRoles();
        for (Role role : roles) {
            auths.add(new SimpleGrantedAuthority(role.getMark()));
        }
        return auths;
    }

    @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 true;
    }
}

UserDetailsService实现类

public class MemberService implements UserDetailsService {

    @Autowired
    MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String name) {

        Member member = memberRepository.findByUsername(name);
        if (member == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }

        return member;
    }
}

两种写法都可,但个人认为这种写法较为繁琐


继续原来的内容

JPA操作类

package com.pan.repository;

import com.pan.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {

    User findByUsername(String username);
}

4. Controller和视图

package com.pan.controller;

import com.pan.entity.User;
import com.pan.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

@Controller
public class IndexController {

    @Autowired
    private UserRepository userRepository;

    @ResponseBody
    @RequestMapping("/personal_center")
    public void login(HttpServletRequest request) {
        System.out.println("登录成功");
    }

    @ResponseBody
    @PostMapping("/registry")
    public void registry(User user) {
        userRepository.save(new User(user.getUsername(), user.getPassword()));
    }
}

登录页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login" method="post">
    姓名:<input type="text" name="username">
    密码:<input type="password" name="password">
    <button type="submit">登录</button>
</form>
</body>
</html>

注册页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/registry" method="post">
    姓名:<input type="text" name="username">
    密码:<input type="password" name="password">
    <button type="submit">注册</button>
</form>
</body>
</html>

界面简陋请见谅

5. Security配置类

在SpringBoot2.0版本以前,我们是这样配置的

package com.pan.security;

import com.pan.service.impl.UserDetailsServiceImpl;
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.builders.WebSecurity;
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;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    UserDetailsService detailsService() {
        return new UserDetailsServiceImpl();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(detailsService());
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/config/**", "/css/**", "/fonts/**", "/img/**", "/js/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers()
                .and().authorizeRequests()
                .antMatchers("/registry").permitAll()
                .anyRequest().authenticated()
                .and().formLogin().loginPage("/sign_in") 
                .loginProcessingUrl("/login").defaultSuccessUrl("/personal_center",true)
                .failureUrl("/sign_in?error").permitAll()
                .and().sessionManagement().invalidSessionUrl("/sign_in")
                .and().rememberMe().tokenValiditySeconds(1209600)
                .and().logout().logoutSuccessUrl("/sign_in").permitAll()
                .and().csrf().disable();
    }
}

具体的配置请自行网上搜索
视图类

package com.pan.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/sign_in").setViewName("login");
        registry.addViewController("/").setViewName("login");
    }
}

在数据库中预先存储用户名和密码,然后登录验证,完全没有问题
但现在,却出问题了,后台报错

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

这是什么鬼?以前可没有这种问题啊

查阅资料得知,SpringBoot2.0抛弃了原来的NoOpPasswordEncoder,要求用户保存的密码必须要使用加密算法后存储,在登录验证的时候Security会将获得的密码在进行编码后再和数据库中加密后的密码进行对比,文献如下:

SpringBoot官方文档

在官方文档中,给出了解决方案,我们可以通过在配置类中添加如下配置来回到原来的写法

@Bean
public static NoOpPasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}

这样能解决办法,但NoOpPasswordEncoder 已经被官方废弃了,既然废弃它肯定是有原因的,而且这种勉强的做法也不符合我们程序员精益求精的风格

正确做法如下:

写法一:

1. 在WebSecurityConfig中定义一个新的bean对象
@Bean
public PasswordEncoder passwordEncoder() {
    // return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    return new BCryptPasswordEncoder();
}

这里使用的是BCryptPasswordEncoder编码方式也可选择其他,如下:

String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);

PasswordEncoderFactories.createDelegatingPasswordEncoder()方法默认返回值为BCryptPasswordEncoder(),两个return等价

2. 修改WebSecurityConfig的一个重写方法

原来为:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(detailsService());
}

修改为:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(detailsService()).passwordEncoder(passwordEncoder());
}

passwordEncoder(passwordEncoder())里面的passwordEncoder()为我们定义的bean
这样在登录的时候就会使用我们选择编码方式进行验证
也可以不写上述方法,但需要在Security配置类中添加以下内容:

@Bean
public DaoAuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
    authenticationProvider.setUserDetailsService(detailsService());
    authenticationProvider.setPasswordEncoder(passwordEncoder());
    return authenticationProvider;
}

将我们定义的PasswordEncoder的Bean和自定义的UserDetailsService注入到DaoAuthenticationProvider,上面所写的修改方法在Security内部也是创建DaoAuthenticationProvider,两者等价

由于使用了编码验证,所以我们需要一组编码后的密码,否则会有如下警告:

o.s.s.c.bcrypt.BCryptPasswordEncoder     : Encoded password does not look like BCrypt

修改IndexController

@Autowired
private PasswordEncoder passwordEncoder;

自动注入一个PasswordEncoder
修改registry方法

@ResponseBody
@PostMapping("/registry")
public void registry(User user) {
    userRepository.save(new User(user.getUsername(), passwordEncoder.encode(user.getPassword())));
}

在密码保存时进行密码编码加密,也可以将加密封装成一个工具类,方便使用,切记封装工具类要用构造方法生成PasswordEncoder 对象,否则会报空指针异常
加密后的密码:
这里写图片描述
这样就可以完美解决问题了,密码的安全性也有了保障

写法二:

1. WebSecurityConfig配置

不定义PasswordEncoder的bean
修改userDetailsService的configure重写方法

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(detailsService()).passwordEncoder(new BCryptPasswordEncoder());
}

若不写该方法,则添加:

@Bean
public DaoAuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
    authenticationProvider.setUserDetailsService(detailsService());
    authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
    return authenticationProvider;
}
2. IndexController

不用自动注入,new一个编码对象,然后使用

private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

总结

到此,整合就初步完成了,整合问题也得到了完美解决,希望我的文章能对你有所帮助

猜你喜欢

转载自blog.csdn.net/SWPU_Lipan/article/details/80586054