zoukankan      html  css  js  c++  java
  • 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.goldwind.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.goldwind.config下创建一个bean配置类,配置加密方式:

    package com.goldwind.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.goldwind.service;
    
    import com.goldwind.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+"
    "+"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.goldwind.controller包下创建LoginController.java:

    package com.goldwind.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.goldwind.handler,在包下创建CustomAuthenticationSucessHandler.java:

    package com.goldwind.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.goldwind.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.goldwind.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.goldwind.config下创建类ErrorPageConfig:

    package com.goldwind.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.goldwind.controller下创建类ErrorController:

    package com.goldwind.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

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

  • 相关阅读:
    win10下安装scrapy不成功的问题解决
    python方法和函数区别
    关于Django 报错 ImportError: cannot import name RegexUrlResolver解决
    Django+Vue后端解决跨域问题
    python中yield的用法
    启动后、路由加载之前定制一段代码(基于admin原理)
    Hadoop期末复习
    python爬虫期末复习
    idea开发环境搭建ssh
    intelliJ破解及JavaEE搭建
  • 原文地址:https://www.cnblogs.com/zyly/p/12286426.html
Copyright © 2011-2022 走看看