SpringSecurity详解

siki学院课程:http://www.sikiedu.com/course/366 (SpringSecurity和SpringSocial认证授权)

SpringSecurity可以理解为一系列的拦截器(filter)。将我们的服务保护起来,访问任何资源都需要身份认证。配置filter,SpringBoot已经帮我们默认配置好了。如果需要自定义配置,我们需要继承WebSecurityConfigurerAdapter类。

一、SpringSecurity依赖

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

二、自定义配置SpringSecurity

配置类继承WebSecurityConfigurerAdapter,重写configure(HttpSecurity http)方法。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单登录(身份认证)
        http.formLogin()
                .and()
                // 请求授权
                .authorizeRequests()
                // 所有请求
                .anyRequest()
                // 都需要身份认证
                .authenticated();
    }
}

编写controller

@RestController
public class UserController {

    @GetMapping("/user")
    public String user(){
        return "user";
    }

}

启动项目

SpringSecurity默认生成了一个登录密码,会在控制台输出。用SpringSecurity自带的登录界面进行认证的时候,必须是这个密码。用户名默认是user。否则认证不成功。
SpringSecurity详解
访问/user请求会被SpringSecurity拦截,跳转到认证界面。
SpringSecurity详解
认证成功之后,成功访问。

三、SpringSecurity原理

SpringSecurity详解

四、SpringSecurity设置密码

实现接口UserDetailsService。这个接口是SpringSecurity默认处理登录的。

public interface UserDetailsService {
   // ~ Methods
   // ========================================================================================================

   /**
    * Locates the user based on the username. In the actual implementation, the search
    * may possibly be case sensitive, or case insensitive depending on how the
    * implementation instance is configured. In this case, the <code>UserDetails</code>
    * object that comes back may have a username that is of a different case than what
    * was actually requested..
    *
    * @param username the username identifying the user whose data is required.
    *
    * @return a fully populated user record (never <code>null</code>)
    *
    * @throws UsernameNotFoundException if the user could not be found or the user has no
    * GrantedAuthority
    */
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

实现UserDetailsService接口

重写的方法的返回值是UserDetails接口,SpringSecurity有默认的实现User类。注意要导SpringSecurity的包。

SpringSecurity详解

/**
 * 用SpringSecurity默认的登录系统
 */
@Service
public class UserServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        /**
         * 用户名、密码、权限
         */
        return new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
    }
}

重新发布项目,测试。密码是123456,用户名任意。发现报错:
SpringSecurity详解
发现原因是设置的密码是明文,SpringSecurity5要求必须要加密。

密码加密处理

SecurityConfig中配置加密方式

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 告诉SpringSecurity密码加密方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单登录(身份认证)
        http.formLogin()
                .and()
                // 请求授权
                .authorizeRequests()
                // 所有请求
                .anyRequest()
                // 都需要身份认证
                .authenticated();
    }
}

service中加密

/**
 * 用SpringSecurity默认的登录系统
 */
@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        /**
         * 用户名、密码、权限
         */
        return new User(username, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
    }
}

再次测试,登录成功。

五、SpringSecurity在数据库中查询用户

配置数据库连接信息和配置JPA

application.yaml

spring:
  # datasource
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springsecurity?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  # jpa
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update

用户实体

参考SpringSecurity默认实现的User代码。只修改我们需要的部分。

@Entity
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;

    /** 用户是否没有失效 **/
    @Transient
    private boolean accountNonExpired;
    /** 用户是否冻结 **/
    @Transient
    private boolean accountNonLocked;
    /** 证明是否过期 **/
    @Transient
    private boolean credentialsNonExpired;
    /** 判断是否删除 **/
    @Transient
    private boolean enabled;
    @Transient
    private Set<GrantedAuthority> authorities;

    /**
     * 给hibernate用的构造方法
     */
    public User() {
    }

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

    /**
     * 给SpringSecurity用的构造方法
     */
    public User(String username, String password,
                Collection<? extends GrantedAuthority> authorities) {
        this(username, password, true, true, true, true, authorities);
    }

    public User(String username, String password, boolean enabled,
                boolean accountNonExpired, boolean credentialsNonExpired,
                boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {

        if (((username == null) || "".equals(username)) || (password == null)) {
            throw new IllegalArgumentException(
                    "Cannot pass null or empty values to constructor");
        }

        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.credentialsNonExpired = credentialsNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
    }

    private static SortedSet<GrantedAuthority> sortAuthorities(
            Collection<? extends GrantedAuthority> authorities) {
        Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
        // Ensure array iteration order is predictable (as per
        // UserDetails.getAuthorities() contract and SEC-717)
        SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet<>(
                new AuthorityComparator());

        for (GrantedAuthority grantedAuthority : authorities) {
            Assert.notNull(grantedAuthority,
                    "GrantedAuthority list cannot contain any null elements");
            sortedAuthorities.add(grantedAuthority);
        }

        return sortedAuthorities;
    }

    private static class AuthorityComparator implements Comparator<GrantedAuthority>,
            Serializable {
        private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

        @Override
        public int compare(GrantedAuthority g1, GrantedAuthority g2) {
            // Neither should ever be null as each entry is checked before adding it to
            // the set.
            // If the authority is null, it is a custom authority and should precede
            // others.
            if (g2.getAuthority() == null) {
                return -1;
            }

            if (g1.getAuthority() == null) {
                return 1;
            }

            return g1.getAuthority().compareTo(g2.getAuthority());
        }
    }

    /**
     * 权限
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }


    /**
     * 用户是否没有失效
     */
    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    /**
     * 用户是否冻结
     */
    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    /**
     * 证明是否过期
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    /**
     * 判断是否删除
     */
    @Override
    public boolean isEnabled() {
        return enabled;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

}

UserServiceImpl修改为自定义的User实体

UserServiceImpl类

@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        /** 自己编写的用户实体类User **/
        User user = userRepository.findUserByUsername(username);
        if (user == null){
            throw new UsernameNotFoundException(username);
        }
        /**
         * 自己编写的用户实体类User
         * 用户名、密码、权限
         **/
        return new User(username, passwordEncoder.encode(user.getPassword()), AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
    }
}

UserRepository类

public interface UserRepository extends CrudRepository<User,Long> {

    /**
     * 根据用户名查找用户
     * @param username
     * @return
     */
    @Query(value = "select * from user where username = ?1", nativeQuery = true)
    User findUserByUsername(String username);

}

测试

运行之后,会在数据库中生成一张表。手动添加一条记录。
SpringSecurity详解
访问/user,会跳转到SpringSecurity的默认登录界面认证。输入数据库中的用户名和密码。发现认证成功。
SpringSecurity详解

总结

密码是SpringSecurity自动完成验证,我们只需要将正确的密码告诉SpringSecurity。我们通过用户名到数据库中查找用户,如果查到,就将查询到的用户密码告诉SpringSecurity。然后SpringSecurity就会将这个密码和默认登录界面接收到的密码对比,一样则认证成功,否则失败。

SpringSecurity详解

扩展

在UserServiceImpl中使用7个参数的构造方法。并修改enabled为false。

@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        /** 自己编写的用户实体类User **/
        User user = userRepository.findUserByUsername(username);
        if (user == null){
            throw new UsernameNotFoundException(username);
        }
        /**
         * 自己编写的用户实体类User
         * 用户名、密码、权限
         * 密码是SpringSecurity自动完成验证,我们只需要将正确的密码告诉SpringSecurity
         **/
        return new User(username, passwordEncoder.encode(user.getPassword()),false,true,true,true, AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
    }
}

测试,输入正确的用户名和密码。此时这4个布尔值会有不同的效果。

enabled = false

SpringSecurity详解

accountNonExpired = false

SpringSecurity详解

credentialsNonExpired = false

SpringSecurity详解

accountNonLocked = false

SpringSecurity详解

利用这4个字段,可以实现对账户的锁定、删除、冻结等等。

六、SpringSecurity自定义登录页面

配置自定义登录页面

login.html name必须为username和password。SpringSecurity底层就是使用的username和password。

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

SecurityConfig配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 告诉SpringSecurity密码加密方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单登录(身份认证)
        http.formLogin()
                // 自定义登录页面
                .loginPage("/login.html")
                .and()
                // 请求授权
                .authorizeRequests()
                // 所有请求
                .anyRequest()
                // 都需要身份认证
                .authenticated();
    }
}

测试

虽然跳转到了自定义的登录界面。但是报错了:
SpringSecurity详解

解决页面无限循环重定向

由于我们配置了所有请求都需要身份认证。我们自己配置的自定义登录页面,同时也是资源,不是SpringSecurity自带的界面,会被拦截。当我们访问请求/user时,首先会重定向到我们自己定义认证界面(login.html)。然而login.html页面还是需要认证,又重定向到认证界面(login.html)。出现了无限循环重定向。所以我们需要设置访问login.html页面不需要身份认证,这样才能使用我们自定义的登录界面来替换SpringSecurity默认的登录界面。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 告诉SpringSecurity密码加密方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单登录(身份认证)
        http.formLogin()
                // 自定义登录页面
                .loginPage("/login.html")
                .and()
                // 请求授权
                .authorizeRequests()
                // 访问URL,不需要身份认证,可以立即访问
                .antMatchers("/login.html").permitAll()
                // 所有请求
                .anyRequest()
                // 都需要身份认证
                .authenticated();
    }
}

访问登录页面成功,但是还不能成功认证,一直在登录页面。
SpringSecurity详解

完成自定义登录页面

SpringSecurity自带的处理用户名密码登录的filter:UsernamePasswordAuthenticationFilter

SpringSecurity详解
SpringSecurity详解

配置SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 告诉SpringSecurity密码加密方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单登录(身份认证)
        http.formLogin()
                // 自定义登录页面
                .loginPage("/login.html")
                // 登录页面的处理url
                .loginProcessingUrl("/loginPage")
                .and()
                // 请求授权
                .authorizeRequests()
                // 访问URL,不需要身份认证,可以立即访问
                .antMatchers("/login.html").permitAll()
                // 所有请求
                .anyRequest()
                // 都需要身份认证
                .authenticated();
    }
}

测试,登录还是没反应。

还需要关闭SpringSecurity自带的跨站请求伪造防护。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 告诉SpringSecurity密码加密方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单登录(身份认证)
        http.formLogin()
                // 自定义登录页面
                .loginPage("/login.html")
                // 如果url为/loginPage,则用SpringSecurity自带的过滤器(UsernamePasswordAuthenticationFilter)来处理该请求
                .loginProcessingUrl("/loginPage")
                .and()
                // 请求授权
                .authorizeRequests()
                // 访问URL,不需要身份认证,可以立即访问
                .antMatchers("/login.html").permitAll()
                // 所有请求
                .anyRequest()
                // 都需要身份认证
                .authenticated()
                .and()
                // 关闭跨站请求伪造防护
                .csrf().disable();
    }
}

认证成功!

七、完成小需求

判断请求是否以html结尾,以html结尾,重定向到登录。不是以html结尾,需要身份认证。

SecurityConfig配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 告诉SpringSecurity密码加密方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单登录(身份认证)
        http.formLogin()
                // 自定义登录页面
                .loginPage("/require")
                // 如果url为/loginPage,则用SpringSecurity自带的过滤器(UsernamePasswordAuthenticationFilter)来处理该请求
                .loginProcessingUrl("/loginPage")
                .and()
                // 请求授权
                .authorizeRequests()
                // 访问URL,不需要身份认证,可以立即访问
                .antMatchers("/login.html","/require").permitAll()
                // 所有请求
                .anyRequest()
                // 都需要身份认证
                .authenticated()
                .and()
                // 关闭跨站请求伪造防护
                .csrf().disable();
    }
}

SecurityController

@RestController
public class SecurityController {

    /** 拿到引发跳转的之前的请求 **/
    private RequestCache requestCache = new HttpSessionRequestCache();

    /** 处理重定向 **/
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @GetMapping("/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public String require(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 获取引发跳转之前的请求
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        // 判断之前的请求是否以html结尾
        if (savedRequest != null){
            // 引发跳转之前的请求
            String url = savedRequest.getRedirectUrl();
            // 以html结尾,重定向到登录
            if (StringUtils.endsWithIgnoreCase(url,".html")){
                redirectStrategy.sendRedirect(request,response,"/login.html");
            }
        }

        // 不是以html结尾,需要身份认证
        return "需要身份认证";
    }

}

测试,首先访问/user:
SpringSecurity详解
访问/user.html,会跳转到登录页面。
SpringSecurity详解
输入正确的用户名和密码之后,由于没有定义/user.html。出现404。
SpringSecurity详解

八、自定义登录成功之后的Handler

SpringSecurity默认登录成功后会跳转到之前的请求。自定义登录成功需要实现AuthenticationSuccessHandler接口。

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Resource
    private ObjectMapper objectMapper;

    /**
     * 登录成功之后调用的函数
     * @param request request
     * @param response response
     * @param authentication 认证信息(发起的认证请求(ip、session)、认证成功之后的用户信息)
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("登陆成功=======>");
        // 将authentication以json的形式返回页面
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }

}

配置SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 告诉SpringSecurity密码加密方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Resource
    private LoginSuccessHandler loginSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单登录(身份认证)
        http.formLogin()
                // 自定义登录页面
                .loginPage("/require")
                // 如果url为/loginPage,则用SpringSecurity自带的过滤器(UsernamePasswordAuthenticationFilter)来处理该请求
                .loginProcessingUrl("/loginPage")
                // 登录成功之后的处理handler
                .successHandler(loginSuccessHandler)
                .and()
                // 请求授权
                .authorizeRequests()
                // 访问URL,不需要身份认证,可以立即访问
                .antMatchers("/login.html","/require").permitAll()
                // 所有请求
                .anyRequest()
                // 都需要身份认证
                .authenticated()
                .and()
                // 关闭跨站请求伪造防护
                .csrf().disable();
    }
}

测试访问/user.html。控制台输出:
SpringSecurity详解
登录成功之后的页面:
SpringSecurity详解

{
    # 用户权限
    "authorities": [
        {
        "authority": "ADMIN"
        }
    ],
	# 认证请求的信息
    "details": {
        "remoteAddress": "0:0:0:0:0:0:0:1",
        "sessionId": "FEFB90B1D0D3681427301D8A0CC04D2B"
    },
	# 用户是否已经通过身份认证 
    "authenticated": true,
	# UserDetails
    "principal": {
        "username": "哔哩哔哩",
        "password": "$2a$10$E0wVWIV0hRdqA0BLZNwvTuT6MIRuAd8KCsNQXXtEi9xqJGiocH.JK",
        "accountNonExpired": true,
        "accountNonLocked": true,
        "credentialsNonExpired": true,
        "enabled": true,
        "authorities": [
            {
            "authority": "ADMIN"
            }
        ]
    },
	# 密码
    "credentials": null,
	# 用户名
    "name": "哔哩哔哩"
}

九、自定义登录失败之后的Handler

SpringSecurity默认登录失败后还是跳转到登录界面。自定义登录失败需要实现AuthenticationFailureHandler接口。

@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 登录失败之后调用的方法
     * @param request request
     * @param response response
     * @param exception 登陆失败产生的异常信息
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("登录失败=======>");
        // 设置返回的状态码
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception));
    }
}

SecurityConfig配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 告诉SpringSecurity密码加密方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    @Autowired
    private LoginFailureHandler loginFailureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单登录(身份认证)
        http.formLogin()
                // 自定义登录页面
                .loginPage("/require")
                // 如果url为/loginPage,则用SpringSecurity自带的过滤器(UsernamePasswordAuthenticationFilter)来处理该请求
                .loginProcessingUrl("/loginPage")
                // 登录成功之后的处理handler
                .successHandler(loginSuccessHandler)
                // 登录成功之后的处理handler
                .failureHandler(loginFailureHandler)
                .and()
                // 请求授权
                .authorizeRequests()
                // 访问URL,不需要身份认证,可以立即访问
                .antMatchers("/login.html","/require").permitAll()
                // 所有请求
                .anyRequest()
                // 都需要身份认证
                .authenticated()
                .and()
                // 关闭跨站请求伪造防护
                .csrf().disable();
    }
}

测试

访问/user.html,跳转到登录界面,随便输入用户名和密码:
SpringSecurity详解
结果:
SpringSecurity详解

十、自定义属性配置文件

Properties属性类

DemoSecurityProperties属性类

@ConfigurationProperties(prefix = "demo.security")
public class DemoSecurityProperties {

    /** LoginType登录的方式,默认为JSON(restful风格) **/
    private LoginType loginType = LoginType.JSON;

    public LoginType getLoginType() {
        return loginType;
    }

    public void setLoginType(LoginType loginType) {
        this.loginType = loginType;
    }
}

LoginType登录方式枚举

public enum LoginType {
    JSON,
    REDIRECT
}

自定义配置让其生效

DemoSecurityConfig配置类

@Configuration
@EnableConfigurationProperties(DemoSecurityProperties.class)
public class DemoSecurityConfig {
}

application.yaml

spring:
  # datasource
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springsecurity?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  # jpa
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update

# 自定义配置属性
demo:
  security:
    login-type: json

登录成功handler中读取自定义的配置

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private DemoSecurityProperties demoSecurityProperties;

    /**
     * 登录成功之后调用的函数
     * @param request request
     * @param response response
     * @param authentication 认证信息(发起的认证请求(ip、session)、认证成功之后的用户信息)
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("登陆成功=======>");
        System.out.println(demoSecurityProperties.getLoginType());
        // 将authentication以json的形式返回页面
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }

}

测试,发现登录成功之后打印出了JSON。
SpringSecurity详解

修改application.yaml

spring:
  # datasource
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springsecurity?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 13518529311
  # jpa
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update

# 自定义配置属性
demo:
  security:
    login-type: redirect

测试发现登录成功之后打印出了REDIRECT。
SpringSecurity详解

现在我们可以通过application.yaml系统配置文件来更改我们自己自定义的属性,这样可以更为方便的提高软件通用性。只需要修改配置即可,不用修改代码。

提高软件通用性

登录成功handler

/**
 * @Desc TODO
 * SavedRequestAwareAuthenticationSuccessHandler为SpringSecurity默认处理成功的类
 * @Author HeJin
 * @Date 2021/3/5 9:32
 */
@Component
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private DemoSecurityProperties demoSecurityProperties;

    /**
     * 登录成功之后调用的函数
     * @param request request
     * @param response response
     * @param authentication 认证信息(发起的认证请求(ip、session)、认证成功之后的用户信息)
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("登陆成功=======>");
        // 自定义的处理方式
        if (LoginType.JSON.equals(demoSecurityProperties.getLoginType())){
            // 将authentication以json的形式返回页面
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        }
        // 系统默认方式
        else {
            // 调用父类中的方法,跳转到之前的页面
            super.onAuthenticationSuccess(request,response,authentication);
        }
    }

}

登录失败handler

/**
 * @Desc TODO
 * SimpleUrlAuthenticationFailureHandler 为SpringSecurity默认处理失败的类
 * @Author HeJin
 * @Date 2021/3/5 9:53
 */
@Component
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler{

    @Autowired
    private ObjectMapper objectMapper;

    /** 自己的配置 **/
    @Autowired
    private DemoSecurityProperties demoSecurityProperties;

    /**
     * 登录失败之后调用的方法
     * @param request request
     * @param response response
     * @param exception 登陆失败产生的异常信息
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("登录失败=======>");
        // 自定义的处理方式
        if (LoginType.JSON.equals(demoSecurityProperties.getLoginType())){
            // 设置返回的状态码
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(exception));
        }
        // 系统默认方式
        else {
            super.onAuthenticationFailure(request,response,exception);
        }

    }
}

application.yaml配置为redirect时,使用SpringSecurity默认实现的方式。

spring:
  # datasource
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springsecurity?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 13518529311
  # jpa
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update

# 自定义配置属性
demo:
  security:
    login-type: redirect

登录成功后跳转到之前的页面:
SpringSecurity详解
登录失败:
SpringSecurity详解

application.yaml配置为json时,使用我们自己实现的,在页面打印json信息。

spring:
  # datasource
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springsecurity?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 13518529311
  # jpa
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update

# 自定义配置属性
demo:
  security:
    login-type: json

登录成功:
SpringSecurity详解
登录失败:
SpringSecurity详解

十一、SpringSecurity记住我的原理

SpringSecurity详解

十二、完成记住我功能

前端登录界面

记住我的name属性必须为remember-me。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h1>登录</h1>
<form action="/loginPage" method="post">
    <!--name必须为username-->
    用户名:<input type="text" name="username">
    <br>
    <!--name必须为password-->
    密码:<input type="text" name="password">
    <br>
    <!--name必须为remember-me-->
    <input name="remember-me" type="checkbox" value="true">
    记住我<br>
    <button type="submit">登录</button>
</form>
</body>
</html>

配置SecurityConfig配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 告诉SpringSecurity密码加密方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    @Autowired
    private LoginFailureHandler loginFailureHandler;

    @Autowired
    private DataSource dataSource;

    @Qualifier("userServiceImpl")
    @Autowired
    private UserDetailsService userDetailsService;

    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        return tokenRepository;
    };

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单登录(身份认证)
        http.formLogin()
                // 自定义登录页面
                .loginPage("/require")
                // 如果url为/loginPage,则用SpringSecurity自带的过滤器(UsernamePasswordAuthenticationFilter)来处理该请求
                .loginProcessingUrl("/loginPage")
                // 登录成功之后的处理handler
                .successHandler(loginSuccessHandler)
                // 登录成功之后的处理handler
                .failureHandler(loginFailureHandler)
                // 记住我
                .and().rememberMe()
                // 配置persistentTokenRepository
                .tokenRepository(persistentTokenRepository())
                // 配置userDetailsService
                .userDetailsService(userDetailsService)
                .and()
                // 请求授权
                .authorizeRequests()
                // 访问URL,不需要身份认证,可以立即访问
                .antMatchers("/login.html","/require").permitAll()
                // 所有请求
                .anyRequest()
                // 都需要身份认证
                .authenticated()
                .and()
                // 关闭跨站请求伪造防护
                .csrf().disable();
    }
}

创建表

JdbcTokenRepositoryImpl类

SpringSecurity详解

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key,token varchar(64) not null, last_used timestamp not null)

SpringSecurity详解

设置过期时间

DemoSecurityProperties自定义属性类

@ConfigurationProperties(prefix = "demo.security")
public class DemoSecurityProperties {

    /** LoginType登录的方式,默认为JSON(restful风格) **/
    private LoginType loginType = LoginType.JSON;

    /** Token过期时间为10小时 **/
    private int rememberMeSecond = 36000;

    public int getRememberMeSecond() {
        return rememberMeSecond;
    }

    public void setRememberMeSecond(int rememberMeSecond) {
        this.rememberMeSecond = rememberMeSecond;
    }

    public LoginType getLoginType() {
        return loginType;
    }

    public void setLoginType(LoginType loginType) {
        this.loginType = loginType;
    }
}

application.yaml

spring:
  # datasource
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springsecurity?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  # jpa
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update

# 自定义配置属性
demo:
  security:
    # 登录的方式
    login-type: json
    # Token过期时间
    remember-me-second: 3600

SecurityConfig配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 告诉SpringSecurity密码加密方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    @Autowired
    private LoginFailureHandler loginFailureHandler;

    @Autowired
    private DataSource dataSource;

    @Qualifier("userServiceImpl")
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private DemoSecurityProperties demoSecurityProperties;

    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        return tokenRepository;
    };

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单登录(身份认证)
        http.formLogin()
                // 自定义登录页面
                .loginPage("/require")
                // 如果url为/loginPage,则用SpringSecurity自带的过滤器(UsernamePasswordAuthenticationFilter)来处理该请求
                .loginProcessingUrl("/loginPage")
                // 登录成功之后的处理handler
                .successHandler(loginSuccessHandler)
                // 登录成功之后的处理handler
                .failureHandler(loginFailureHandler)
                // 记住我
                .and().rememberMe()
                // 配置persistentTokenRepository
                .tokenRepository(persistentTokenRepository())
                // 设置Token过期秒数
                .tokenValiditySeconds(demoSecurityProperties.getRememberMeSecond())
                // 配置userDetailsService
                .userDetailsService(userDetailsService)
                .and()
                // 请求授权
                .authorizeRequests()
                // 访问URL,不需要身份认证,可以立即访问
                .antMatchers("/login.html","/require").permitAll()
                // 所有请求
                .anyRequest()
                // 都需要身份认证
                .authenticated()
                .and()
                // 关闭跨站请求伪造防护
                .csrf().disable();
    }
}

测试记住我功能

访问/user.html,会跳转到登录界面,进行认证。勾选记住我。
SpringSecurity详解
登录成功之后,会发现数据库中的表persistent_logins多了一条数据。
SpringSecurity详解
重新启动项目之后。我们再次访问/user.html页面,发现不需要登录了。报404错误,因为没有定义这个页面。
SpringSecurity详解
访问/user,也不需要认证,直接访问页面。
SpringSecurity详解

这时候所有需要认证的界面都能访问了,不需要认证。记住我功能就实现了。因为使用的是Cookies,只要Token没过期或者数据库中有记录,不管是关闭浏览器还是重启项目,都不需要再次认证。如果用另一个公司的浏览器打开,会需要认证,因为数据库中保存的Token和浏览器中的不一样。

上一篇:SpringSecurity


下一篇:企业级实战——品优购电商系统开发-102 .103 .密码加密-配置 显示登录名与退出登录