程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

Spring Security -- 自定义用户认证(转载)

Spring Security -- Spring Boot中开启Spring Security一节中我们简单搭建了个Spring Boot + Spring Security的项目,认证的用户名和密码都是由Spring Security生成。Spring Security支持我们自定义认证的过程,如处理用户信息获取逻辑,使用我们自定义的登录页面替换Spring Security默认的登录页及自定义登录成功或失败后的处理逻辑等。这里将在上一节的源码基础上进行改造。

一、自定义认证过程

1、UserDetailService接口和UserDetails接口

自定义认证的过程需要实现Spring Security提供的UserDetailService接口,该接口只有一个抽象方法loadUserByUsername,源码如下:

package org.springframework.security.core.userdetails;

/**
 * Core interface which loads user-specific data.
 * <p>
 * It is used throughout the framework as a user DAO and is the strategy used by the
 * {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider
 * DaoAuthenticationProvider}.
 *
 * <p>
 * The interface requires only one read-only method, which simplifies support for new
 * data-access strategies.
 *
 * @see org.springframework.security.authentication.dao.DaoAuthenticationProvider
 * @see UserDetails
 *
 * @author Ben Alex
 */
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;
}

loadUserByUsername方法返回一个UserDetails对象,UserDetails也是一个接口,包含一些用于描述用户信息的方法,源码如下:

/**
 * Provides core user information.
 *
 * <p>
 * Implementations are not used directly by Spring Security for security purposes. They
 * simply store user information which is later encapsulated into {@link Authentication}
 * objects. This allows non-security related user information (such as email addresses,
 * telephone numbers etc) to be stored in a convenient location.
 * <p>
 * Concrete implementations must take particular care to ensure the non-null contract
 * detailed for each method is enforced. See
 * {@link org.springframework.security.core.userdetails.User} for a reference
 * implementation (which you might like to extend or use in your code).
 *
 * @see UserDetailsService
 * @see UserCache
 *
 * @author Ben Alex
 */
public interface UserDetails extends Serializable {
    // ~ Methods
    // ========================================================================================================

    /**
     * Returns the authorities granted to the user. Cannot return <code>null</code>.
     *
     * @return the authorities, sorted by natural key (never <code>null</code>)
     */
    Collection<? extends GrantedAuthority> getAuthorities();

    /**
     * Returns the password used to authenticate the user.
     *
     * @return the password
     */
    String getPassword();

    /**
     * Returns the username used to authenticate the user. Cannot return <code>null</code>.
     *
     * @return the username (never <code>null</code>)
     */
    String getUsername();

    /**
     * Indicates whether the user's account has expired. An expired account cannot be
     * authenticated.
     *
     * @return <code>true</code> if the user's account is valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    boolean isAccountNonExpired();

    /**
     * Indicates whether the user is locked or unlocked. A locked user cannot be
     * authenticated.
     *
     * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
     */
    boolean isAccountNonLocked();

    /**
     * Indicates whether the user's credentials (password) has expired. Expired
     * credentials prevent authentication.
     *
     * @return <code>true</code> if the user's credentials are valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    boolean isCredentialsNonExpired();

    /**
     * Indicates whether the user is enabled or disabled. A disabled user cannot be
     * authenticated.
     *
     * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
     */
    boolean isEnabled();
}

这些方法的含义如下:、

  • getAuthorities获取用户包含的权限,返回权限集合,权限是一个继承了GrantedAuthority的对象;
  • getPassword和getUsername用于获取密码和用户名;
  • isAccountNonExpired方法返回boolean类型,用于判断账户是否未过期,未过期返回true反之返回false;
  • isAccountNonLocked方法用于判断账户是否未锁定;
  • isCredentialsNonExpired用于判断用户凭证是否没过期,即密码是否未过期;
  • isEnabled方法用于判断用户是否可用;

实际中我们可以自定义UserDetails接口的实现类,也可以直接使用Spring Security提供的UserDetails接口实现类org.springframework.security.core.userdetails.User。

2、自定义CustomUserDetailService和User

说了那么多,下面我们来开始实现UserDetailService接口的loadUserByUsername方法。

首先创建UserDetails接口的实现类User,用于存放模拟的用户数据(实际中一般从数据库获取,这里为了方便直接模拟):

package com.zy.example.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.*;

/**
 * @Author: zy
 * @Description: 用户实体类
 * Spring Security框架提供了一个基础用户接口UserDetails,该接口提供了基本的用户相关的操作,比如获取用户名/密码、
 * 用户账号是否过期和用户认证是否过期等,我们定义自己的User类时需要实现该接口。
 * @Date: 2020-2-9
 */
@Data
@NoArgsConstructor
public class User implements UserDetails {

    private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();

    private String id;

    /**
     * 用户登录名
     */
    private String username;

    /**
     * 用户真实姓名
     */
    private String realName;

    /**
     * 用户登录密码,用户的密码不应该暴露给客户端
     */
    @JsonIgnore
    private String password;

    /**
     * 用户创建者
     */
    private int createdBy;

    /**
     * 创建时间
     */
    private Long createdTime = System.currentTimeMillis();

    /**
     * 该用户关联的企业/区块id
     */
    private Map<String, Object> associatedResources = new HashMap<>();

    /**
     * 用户关注的企业列表
     */
    private List<String> favourite = new ArrayList<>();

    /**
     * 用户在系统中的角色列表,将根据角色对用户操作权限进行限制
     */
    private List<String> roles = new ArrayList<>();


    /**
     * 设置密码
     * @param password
     */
    public void setPassword(String password) {
        this.password = PASSWORD_ENCODER.encode(password);
    }

    /**
     * 权限集合
     */
    private Collection<? extends GrantedAuthority> authorities = null;

    /**
     * 账户是否未过期
     */
    private boolean accountNonExpired = true;

    /**
     * 账户是否未锁定
     */
    private boolean accountNonLocked= true;

    /**
     * 用户凭证是否没过期,即密码是否未过期
     */
    private boolean credentialsNonExpired= true;

    /**
     * 用户是否可用
     */
    private boolean enabled= true;
}

PasswordEncoder是一个密码加密接口,而BCryptPasswordEncoder是Spring Security提供的一个实现方法,我们也可以自己实现PasswordEncoder。不过Spring Security实现的BCryptPasswordEncoder已经足够强大,它对相同的密码进行加密后可以生成不同的结果。

此外,我们在com.zy.example.config下创建一个bean配置类,配置加密方式:

package com.zy.example.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @Author: zy
 * @Description: 定义一些bean
 * @Date: 2020-2-9
 */
@Configuration
public class BeanConfig {
    /**
     * 密码加密
     * @return
     */
    @Bean
    public static PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

接着创建类CustomUserDetailService实现UserDetailService接口:

package com.zy.example.service;

import com.zy.example.entity.User;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.authority.AuthorityUtils;
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;

/**
 * @Author: zy
 * @Description: 自定义用户信息Service配置类
 * @Date: 2020-2-9
 */
@Service
public class CustomUserDetailsService implements UserDetailsService {
    /**
     * 点击登录时会调用该函数、并传入登录名  根据用户名查询数据库获取用户信息
     * @param username:登录用户名
     * @return: 返回用户信息
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //模拟一个用户 替代数据库获取逻辑
        User user = new User();
        user.setUsername(username);
        user.setPassword("123456");
        // 输出加密后的密码
        System.out.println(user.getPassword());


        return new org.springframework.security.core.userdetails.User(user.getUsername(),
                user.getPassword(),
                user.isEnabled(),
                user.isAccountNonExpired(),
                user.isCredentialsNonExpired(),
                user.isAccountNonLocked(),
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

这里我们使用了org.springframework.security.core.userdetails.User类包含7个参数的构造器,其还包含一个三个参数的构造器User(String username, String password,Collection<? extends GrantedAuthority> authorities),由于权限参数不能为空,所以这里先使用AuthorityUtils.commaSeparatedStringToAuthorityList方法模拟一个admin的权限,该方法可以将逗号分隔的字符串转换为权限集合。

这时候重启项目,访问http://localhost:8080/login,便可以使用任意用户名以及123456作为密码登录系统。我们多次进行登录操作,可以看到控制台输出的加密后的密码如下:

$2a$10$QWhO2OtA6/o0c6P2/KIwzOIlS5xGpPHrYxbeVc8AvAf0LfmZaLCfq
$2a$10$3A6L/hDeb9OeM/5KzUMfHufwZtqTuV5gyi2vHN6N2w8U7TrA9GQa2
$2a$10$gWzh2cqGqYg4qzH8lmYlUeHWc8epTyh6.gMyVdW4xZDJLNU4s1pnW

可以看到,BCryptPasswordEncoder对相同的密码生成的结果每次都是不一样的。

二、替换默认表单页面

默认的登录页面过于简陋,我们可以自己定义一个登录页面。

1、使用Freemarker模板引擎渲染Web视图

pom文件引入依赖包:

<!--   引入freemarker的依赖包   -->
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

在src/main/resources/创建一个templates文件夹,并创建login.ftl文件:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
    <style type="text/css">
        * {
            margin: 0px;
        }

        #content {
            margin: 150px auto;
            width: 100%;
            height: 460px;
            border: 1px transparent solid;
            background-color: #21D4FD;
            background-image: linear-gradient(243deg, #21D4FD 0%, #B721FF 100%);
            background-image: -webkit-linear-gradient(243deg, #21D4FD 0%, #B721FF 100%);
            background-image: -moz-linear-gradient(243deg, #21D4FD 0%, #B721FF 100%);
            background-image: -o-linear-gradient(243deg, #21D4FD 0%, #B721FF 100%);
        }

        #box {
            margin: 50px auto;
            width: 30%;
            height: 360px;
            background-color: #fff;
            text-align: center;
            border-radius: 15px;
            border: 2px #fff solid;
            box-shadow: 10px 10px 5px #000000;
        }

        .title {
            line-height: 58px;
            margin-top: 20px;
            font-size: 36px;
            color: #000;
            height: 58px;
        }

        #box:hover {
            border: 2px #fff solid;
        }

        .input {
            margin-top: 20px;
        }

        input {
            margin-top: 5px;
            outline-style: none;
            border: 1px solid #ccc;
            border-radius: 3px;
            padding: 13px 14px;
            width: 70%;
            font-size: 14px;
            font-weight: 700;
            font-family: "Microsoft soft";
        }

        button {
            margin-top: 20px;
            border: none;
            color: #000;
            padding: 15px 32px;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 16px;
            border-radius: 15px;
            background-color: #CCCCCC;
        }
        button:hover{
            background-color: #B721FF;
            color: #fff;
        }
    </style>
</head>
<body>
    <div id="content">
        <div id="box">
            <div class="title">Login</div>
            <div class="input">
                <form name="f" action="/login" method="post">
                    <input type="text" id="username" name="username" value="" placeholder="用户名" />
                    <br>
                    <input type="password" id="password" name="password" placeholder="密码" />
                    <br>
                    <input type="submit" value="登录" onclick="getuser()"/>
                </form>
            </div>
        </div>
    </div>

    <script type="text/javascript">
        function getuser() {
            var username = document.getElementById("username").value;
            var password = document.getElementById("password").value;
            var password1 = document.getElementById("password1").value;
            testing(username, password,password1)
            //alert("username:"+username+"\n"+"password:"+password);
        }

        function testing(username, password, password1) {
            var tmp = username && password;
            if (tmp == "") {
                alert("请填写完整信息");
                return 0;
            }
            if (username.length < 6 || username.length > 16) {
                alert("用户名长度为:6-16位")
                return 0;
            }
            if (password<6)
            {
                alert("密码长度错误");
            }
        }
    </script>
</body>
</html>
View Code

在src/main/resources下新建freemarker配置文件application.yml:

spring:
  ## Freemarker 配置
  freemarker:
    ##模版存放路径(默认为 classpath:/templates/)
    template-loader-path: classpath:/templates/
    ##是否生成缓存,生成环境建议开启(默认为true)
    cache: false
    ##编码
    charset: UTF-8
    check-template-location: true
    ##content-type类型(默认为text/html)
    content-type: text/html
    ## 设定所有request的属性在merge到模板的时候,是否要都添加到model中(默认为false)
    expose-request-attributes: false
    ##设定所有HttpSession的属性在merge到模板的时候,是否要都添加到model中.(默认为false)
    expose-session-attributes: false
    ##RequestContext属性的名称(默认为-)
    request-context-attribute: request
    ##模板后缀(默认为.ftl)
    suffix: .ftl

2、LoginController

在com.zy.example.controller包下创建LoginController.java:

package com.zy.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @Author: zy
 * @Description: 登陆页面
 * @Date: 2020-2-9
 */
@Controller
public class LoginController {

    /**
     * 自定义登录页面
     * @return
     */
    @RequestMapping("/login")
    public String login(){
        return "/login";
    }
    
}

3、修改BrowserSecurityConfig配置

要怎么做才能让Spring Security跳转到我们自己定义的登录页面呢?很简单,只需要在类BrowserSecurityConfig的configure中添加一些配置:

    /**
     * 配置拦截请求资源
     * @param http:HTTP请求安全处理
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()    // 授权配置
                .anyRequest()       // 任何请求
             .authenticated()    //都需要身份认证
                .and()
                .formLogin()         // 或者httpBasic()
               .loginPage("/login")  // 指定登录页的路径
                .loginProcessingUrl("/login")  // 指定自定义form表单请求的路径
                // 必须允许所有用户访问我们的登录页(例如未验证的用户,否则验证流程就会进入死循环)
                // 这个formLogin().permitAll()方法允许所有用户基于表单登录访问/login这个page。
                .permitAll()
                .and()
                .logout()
                .permitAll()
                .and()
                //默认都会产生一个hiden标签 里面有安全相关的验证 防止请求伪造 这边我们暂时不需要 可禁用掉
                .csrf().disable();
    

面代码中.loginPage("/login")指定了跳转到登录页面的请求URL,.loginProcessingUrl("/login")对应登录页面form表单的action="/login",如果两者不一样,UsernamePasswordAuthenticationFilter过滤器将不会生效,.permitAll()表示跳转到登录页面的请求不被拦截,否则会进入无限循环。

这时候启动系统,访问http://localhost:8080/hello,会看到页面已经被重定向到了http://localhost:8080/login

 

输入admin、123456,跳转到/hello页面:

 三、处理登录成功和失败

Spring Security有一套默认的处理登录成功和失败的方法:当用户登录成功时,页面会跳转到引发登录的页面,比如在未登录的情况下访问http://localhost:8080/hello,页面会跳转到登录页,登录成功后再跳转回来;登录失败时则是跳转到Spring Security默认的错误提示页面。下面我们通过一些自定义配置来替换这套默认的处理机制。

1、自定义登录成功逻辑

要改变默认的处理成功逻辑很简单,只需要实现org.springframework.security.web.authentication.AuthenticationSuccessHandler接口的onAuthenticationSuccess方法即可:

首先添加jackson依赖:

        <!--    对象json转换    -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.8.3</version>
        </dependency>

创建包com.zy.example.handler,在包下创建CustomAuthenticationSucessHandler.java:

package com.zy.example.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Service;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author: zy
 * @Description: 自定义登录成功逻辑
 * @Date: 2020-2-9
 */
@Service
public class CustomAuthenticationSucessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper mapper;

    /**
     * 登录成功
     * @param request:请求
     * @param response:响应
     * @param authentication:Authentication参数既包含了认证请求的一些信息,比如IP,请求的SessionId等,
     *                      也包含了用户信息
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(mapper.writeValueAsString(authentication));
    }
}

其中Authentication参数既包含了认证请求的一些信息,比如IP,请求的SessionId等,也包含了用户信息,即前面提到的User对象。通过上面这个配置,用户登录成功后页面将打印出Authentication对象的信息。

此外我们注入了mapper对象,该对象用于将Authentication对象json序列化,注入前需要手动配置。我们在bean配置类配置它:

    /**
     * 对象Json序列化
     * @return
     */
    @Bean
    public ObjectMapper mapper() {
        return new ObjectMapper();
    }

为了使CustomAuthenticationSucessHandler生效,我们还的在BrowserSecurityConfig的configure中配置:

    @Autowired
    private AuthenticationSuccessHandler authenticationSucessHandler
  
/**
     * 配置拦截请求资源
     * @param http:HTTP请求安全处理
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()    // 授权配置
           .anyRequest()       // 任何请求
            .authenticated()    //都需要身份认证
               .and()
               .formLogin()         // 或者httpBasic()
               .loginPage("/login")  // 指定登录页的路径
            .loginProcessingUrl("/login")  // 指定自定义form表单请求的路径
               .successHandler(authenticationSucessHandler)    // 处理登录成功
                // 必须允许所有用户访问我们的登录页(例如未验证的用户,否则验证流程就会进入死循环)
                // 这个formLogin().permitAll()方法允许所有用户基于表单登录访问/login这个page。
                .permitAll()
                .and()
                .logout()
                .permitAll()
                .and()
                //默认都会产生一个hiden标签 里面有安全相关的验证 防止请求伪造 这边我们暂时不需要 可禁用掉
                .csrf().disable();
    }

我们将CustomAuthenticationSucessHandler注入进来,并通过successHandler方法进行配置。

这时候重启项目登录后页面将会输出如下JSON信息:

{
    "authorities": [{
        "authority": "admin"
    }],
    "details": {
        "remoteAddress": "127.0.0.1",
        "sessionId": "8C6774C31B224228BCC19CE5F44DA432"
    },
    "authenticated": true,
    "principal": {
        "password": null,
        "username": "admin",
        "authorities": [{
            "authority": "admin"
        }],
        "accountNonExpired": true,
        "accountNonLocked": true,
        "credentialsNonExpired": true,
        "enabled": true
    },
    "credentials": null,
    "name": "admin"
}

像password,credentials这些敏感信息,Spring Security已经将其屏蔽。

除此之外,我们也可以在登录成功后做页面的跳转,修改CustomAuthenticationSucessHandler:

package com.zy.example.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Service;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author: zy
 * @Description: 自定义登录成功逻辑
 * @Date: 2020-2-9
 */
@Service
public class CustomAuthenticationSucessHandler implements AuthenticationSuccessHandler {

    private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    private ObjectMapper mapper;

    /**
     * 登录成功
     * @param request:请求
     * @param response:响应
     * @param authentication:Authentication参数既包含了认证请求的一些信息,比如IP,请求的SessionId等,
     *                      也包含了用户信息
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl());
    }
}

其中HttpSessionRequestCache为Spring Security提供的用于缓存请求的对象,通过调用它的getRequest方法可以获取到本次请求的HTTP信息。DefaultRedirectStrategy的sendRedirect为Spring Security提供的用于处理重定向的方法。

通过上面配置,登录成功后页面将跳转回引发跳转的页面。如果想指定跳转的页面,比如跳转到/index,可以将savedRequest.getRedirectUrl()修改为/index,修改TestController类,添加如下方法:

    @GetMapping("index")
    public Object index(){
        return SecurityContextHolder.getContext().getAuthentication();
    }

登录成功后,便可以使用SecurityContextHolder.getContext().getAuthentication()获取到Authentication对象信息。除了通过这种方式获取Authentication对象信息外,也可以使用下面这种方式:

 @GetMapping("index")
    public Object index(Authentication authentication) {
        return authentication;
    }

重启项目,登录成功后,页面将跳转到http://localhost:8080/index

{
    "authorities": [{
        "authority": "admin"
    }],
    "details": {
        "remoteAddress": "127.0.0.1",
        "sessionId": "8C6774C31B224228BCC19CE5F44DA432"
    },
    "authenticated": true,
    "principal": {
        "password": null,
        "username": "admin",
        "authorities": [{
            "authority": "admin"
        }],
        "accountNonExpired": true,
        "accountNonLocked": true,
        "credentialsNonExpired": true,
        "enabled": true
    },
    "credentials": null,
    "name": "admin"
}

2、自定义登录失败逻辑

和自定义登录成功处理逻辑类似,自定义登录失败处理逻辑需要实现org.springframework.security.web.authentication.AuthenticationFailureHandler的onAuthenticationFailure方法::

@Service
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {
    }
}

onAuthenticationFailure方法的AuthenticationException参数是一个抽象类,Spring Security根据登录失败的原因封装了许多对应的实现类,查看AuthenticationException的Hierarchy:

不同的失败原因对应不同的异常,比如用户名或密码错误对应的是BadCredentialsException,用户不存在对应的是UsernameNotFoundException,用户被锁定对应的是LockedException等。

假如我们需要在登录失败的时候返回失败信息,可以这样处理:

package com.zy.example.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Service;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author: zy
 * @Description: 自定义登录失败逻辑
 * @Date: 2020-2-9
 */
@Service
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper mapper;
    /**
     * 登录失败 返回错误状态码
     * @param request
     * @param response
     * @param exception
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(mapper.writeValueAsString(exception.getMessage()));
    }
}

状态码定义为500(HttpStatus.INTERNAL_SERVER_ERROR.value()),即系统内部异常。

同样的,我们需要在BrowserSecurityConfig的configure中配置它:

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
/** * 配置拦截请求资源 * @param http:HTTP请求安全处理 * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 授权配置        .anyRequest() // 任何请求        .authenticated() //都需要身份认证 .and() .formLogin() // 或者httpBasic()        .loginPage("/login") // 指定登录页的路径        .loginProcessingUrl("/login") // 指定自定义form表单请求的路径        .successHandler(authenticationSucessHandler) // 处理登录成功        .failureHandler(authenticationFailureHandler) // 处理登录失败 // 必须允许所有用户访问我们的登录页(例如未验证的用户,否则验证流程就会进入死循环) // 这个formLogin().permitAll()方法允许所有用户基于表单登录访问/login这个page。 .permitAll() .and() .logout() .permitAll() .and() //默认都会产生一个hiden标签 里面有安全相关的验证 防止请求伪造 这边我们暂时不需要 可禁用掉 .csrf().disable(); }

重启项目,当输入错误的密码时,页面输出如下:

四、修改错误页面

当我们登录一个不存在页面时,http://localhost:8080/user,将会抛出404错误,如何修改这些默认错误页面呢:

1、创建ErrorPageConfig配置类

在包com.zy.example.config下创建类ErrorPageConfig:

package com.zy.example.config;

import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;

/**
 * @Author: zy
 * @Description: spring boot 错误页面配置
 * @Date: 2020-2-8
 */
@Configuration
public class ErrorPageConfig implements ErrorPageRegistrar {
    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {
        ErrorPage errorPage400 = new ErrorPage(HttpStatus.BAD_REQUEST,"/error/400");
        ErrorPage errorPage401 = new ErrorPage(HttpStatus.UNAUTHORIZED,"/error/401");
        ErrorPage errorPage403 = new ErrorPage(HttpStatus.FORBIDDEN,"/error/403");
        ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND,"/error/404");
        ErrorPage errorPage415 = new ErrorPage(HttpStatus.UNSUPPORTED_MEDIA_TYPE,"/error/415");
        ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR,"/error/500");
        registry.addErrorPages(errorPage400,errorPage401,errorPage403,errorPage404,errorPage415,errorPage500);
    }
}

2、Controller

在com.zy.example.controller下创建类ErrorController:

package com.zy.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @Author: zy
 * @Description: spring boot 错误页面配置
 * @Date: 2020-2-8
 */
@Controller
@RequestMapping("/error")
public class ErrorController {
    @RequestMapping("/403")
    public String error403(){
        return "/error/403";
    }

    @RequestMapping("/404")
    public String error404(){
        return "/error/404";
    }
}

3、新增ftl文件

在src/java/resource/templates/error新建错误页面:

403页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>403</title>
    <style>
        html, body {
            padding: 0;
            margin: 0;
            height: 100%;
        }
        .box {
            width: 100%;
            height: 100%;
            background-color: wheat;
            text-align: center;  /*文本水平居中*/
            line-height: 600px;  /*文本垂直居中*/
        }
    </style>
</head>
<body>

<div class="box">
    <h1 style="display: inline">Sorry, this page is Authorised by </h1>
    <h1 style="display: inline"><a href="/login">zy</a></h1>
    <h1 style="display: inline"> only.</h1>
</div>

</body>
</html>
View Code

404页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>404</title>
    <style>
        html, body {
            padding: 0;
            margin: 0;
            height: 100%;
        }
        .box {
            width: 100%;
            height: 100%;
            background-color: wheat;
            text-align: center;  /*文本水平居中*/
            padding-top: 15%;
        }
    </style>
</head>
<body>

<div class="box">
    <h1>404 您进入了无人区...</h1>
    <span id="counter"></span>秒后 <a href="/login">返回登录首页</a>
</div>

<script>
        var $counter = document.getElementById('counter');
        function countDown(secs)
        {
            $counter.innerText=secs;
            if(--secs>0)
            {
                setTimeout("countDown("+secs+")",1000);
            }
            if(secs==0)
            {
                location.href = '/login';
            }
        }
        countDown(5);
    </script>

</body>
</html>
View Code

四、代码下载

springsecurity

参考文章:
[1] Spring Security自定义用户认证

posted @ 2020-02-09 11:07  大奥特曼打小怪兽  阅读(4325)  评论(2编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步