首頁(yè)技術(shù)文章正文

web前端培訓(xùn):SpringSecurity登陸失敗后錯(cuò)誤信息回顯

更新時(shí)間:2022-11-17 來(lái)源:黑馬程序員 瀏覽量:

IT培訓(xùn)班

  一、文章導(dǎo)讀

  SpringSecurity是一個(gè)能夠?yàn)榛赟pring的企業(yè)應(yīng)用系統(tǒng)提供聲明式的安全訪問(wèn)控制解決方案的安全框架。當(dāng)我們使用SpringSecurity自定義登錄頁(yè)面的時(shí)候,如果發(fā)生登陸失敗的情況,如何在自己的頁(yè)面上顯示相應(yīng)的錯(cuò)誤信息?

  針對(duì)上面所描述的問(wèn)題,本次我們將通過(guò)具體的案例來(lái)將問(wèn)題進(jìn)行描述并提出對(duì)應(yīng)的解決方案,我們將從如下方面進(jìn)行具體講解:

  SpringSecurity環(huán)境搭建

  更換自己的登錄頁(yè)面

  在登錄頁(yè)面顯示錯(cuò)誤信息

  自定義用戶名不存在的錯(cuò)誤信息

  自定義用戶密碼錯(cuò)誤的錯(cuò)誤信息

  二、SpringSecurity環(huán)境搭建

       1.創(chuàng)建maven工程項(xiàng)目

  使用IDEA進(jìn)行Maven項(xiàng)目的創(chuàng)建,并將項(xiàng)目構(gòu)建為web項(xiàng)目,創(chuàng)建方式如下:

1668656902441_1.jpg

  2.在項(xiàng)目的pom.xml添加依賴

<dependencies>
    <!--SpringMVC與Spring相關(guān)jar包略 -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>4.1.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>4.1.0.RELEASE</version>
    </dependency>
</dependencies>

  3.web.xml添加委托過(guò)濾器代理類(lèi)

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

  4.添加springSecurity的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:security="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security.xsd">
    <!--配置放行資源 -->
    <security:http pattern="/login.html" security="none"/>
    <security:http pattern="/js/**" security="none"/>
    <!--配置攔截規(guī)則 -->
    <security:http use-expressions="false">
        <security:intercept-url pattern="/**" access="ROLE_ADMIN"/>
        <!--采用默認(rèn)springSecurity提供的默認(rèn)登陸頁(yè)面 -->
        <security:form-login/>
        <security:csrf disabled="true"/>
    </security:http>
    <security:authentication-manager>
        <security:authentication-provider user-service-ref="userDetailService"/>
    </security:authentication-manager>
    <bean id="userDetailService" class="UserDetailService實(shí)現(xiàn)類(lèi)"></bean>
</beans>

  5.添加UserDetailsService與TbUser

  UserService類(lèi)

public class UserService implements UserDetailsService {
    public static Map<String, TbUser> users = new HashMap<String, TbUser>();

    static {
        users.put("root", new TbUser(1, "root", "root", "ROOT"));
        users.put("admin", new TbUser(1, "admin", "admin", "ADMIN"));
    }

    public UserDetails loadUserByUsername(String username) throws
            UsernameNotFoundException {
        if (users.containsKey(username)) {
            //查詢到用戶信息
            TbUser tbUser = users.get(username);
            List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
            list.add(new SimpleGrantedAuthority(tbUser.getRole()));
            return new User(username, tbUser.getPassword(), list);
        } else {
            //未查詢到用戶信息
            return null;
        }
    }
}

  TbUser類(lèi)

public class TbUser {
    private Integer id;
    private String username;
    private String password;
    private String role;
    //setter...getter...constructor...省略
}

  6.運(yùn)行測(cè)試

1668657023027_2.jpg

  三、更換自己的登錄頁(yè)面

      1.修改springsecurity.xml

  在配置文件中添加security:form-login相關(guān)配置,配置內(nèi)容如下:

<security:http use-expressions="false">
    <security:intercept-url pattern="/**" access="ROLE_ADMIN"/>
    <!--采用默認(rèn)springSecurity提供的默認(rèn)登陸頁(yè)面 -->
    <!--<security:form-login/>-->
    <!--更換自己的登陸頁(yè)面
        login-page:設(shè)定登陸login.html頁(yè)面
        default-target-url:默認(rèn)登錄成功后跳轉(zhuǎn)的url
        authentication-failure-forward-url:登陸失敗后跳轉(zhuǎn)的頁(yè)面
    -->
    <security:form-login login-page="/login.html"
        default-target-url="/index.html"
        authentication-failure-url="/login.html"
    />
    <security:csrf disabled="true"/>
</security:http>

  2.在webapp下添加登錄頁(yè)面

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <script type="text/javascript" src="jquery.min.js"></script>
        <script type="text/javascript">
        </script>
    </head>
    <body>
        <h1>自定義登陸頁(yè)面</h1>
        <form action="/login" method="post">
            用戶姓名:<input type="text" name="username"/><br/>
            用戶密碼:<input type="password" name="password"/><br/>
            <input type="submit" value="登陸"/>
        </form>
    </body>
</html>

  3.啟動(dòng)服務(wù),運(yùn)行測(cè)試

1668657176529_3.jpg

  遇到問(wèn)題后,如何在登陸失敗后像SpringSecurity原帶登陸頁(yè)面那樣在頁(yè)面上顯示出對(duì)應(yīng)的錯(cuò)誤信息呢?

  四、在登錄頁(yè)面顯示錯(cuò)誤信息

  1.改springsecurity.xml配置文件

1668657220879_4.jpg

  添加上述內(nèi)容后,當(dāng)再次訪問(wèn)頁(yè)面,如果用戶名或密碼輸入錯(cuò)誤,則會(huì)重新跳回到登陸頁(yè)面,并且會(huì)在瀏覽器上把error=true顯示在瀏覽器中,如下圖所示:

  

1668657267217_5.jpg

  在頁(yè)面加載完后,獲取瀏覽器URL后面的error屬性,判斷如果值為true,則說(shuō)明登陸失敗,發(fā)送請(qǐng)求從后臺(tái)獲取登陸錯(cuò)誤信息,修改代碼如下:

1668657283215_6.jpg

  2.在后臺(tái)提供查詢錯(cuò)誤信息服務(wù)

  編寫(xiě)Controller類(lèi),用來(lái)提供前端頁(yè)面對(duì)錯(cuò)誤信息查詢的服務(wù)

@RestController
@RequestMapping("/login")
public class LoginController {
    @RequestMapping("/getErrorMsg")
    public Map getErrorMsg(HttpSession session){
        Map map = new HashMap();
        Exception e =(Exception)session.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
        map.put("error",e.getMessage());
        return map;
    }
}

  3.啟動(dòng)服務(wù),運(yùn)行測(cè)試

1668657402745_7.jpg

  雖然錯(cuò)誤信息能在頁(yè)面上進(jìn)行展示錯(cuò)誤信息,但是錯(cuò)誤信息為英文,如何將錯(cuò)誤信息轉(zhuǎn)換成中文呢?

  五、自定義用戶名不存在的錯(cuò)誤信息

      1.更改錯(cuò)誤信息

  在UserService中,當(dāng)用戶名不存在,可以通過(guò)拋出異常的方式來(lái)更改錯(cuò)誤信息,修改代碼如下:

public class UserService implements UserDetailsService {
    public static Map<String, TbUser> users = new HashMap<String, TbUser>();

    static {
        users.put("root", new TbUser(1, "root", "root", "ROOT"));
        users.put("admin", new TbUser(1, "admin", "admin", "ADMIN"));
    }

    public UserDetails loadUserByUsername(String username) throws
            UsernameNotFoundException {
        if (users.containsKey(username)) {
            //查詢到用戶信息
            TbUser tbUser = users.get(username);
            List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
            list.add(new SimpleGrantedAuthority(tbUser.getRole()));
            return new User(username, tbUser.getPassword(), list);
        } else {
            //未查詢到用戶信息
            //return null;對(duì)比上述代碼,修改內(nèi)容如下
            throw new UsernameNotFoundException("用戶名不存在");
        }
    }
}

  2.啟動(dòng)服務(wù),運(yùn)行測(cè)試

  重新啟動(dòng)服務(wù)器,測(cè)試后會(huì)發(fā)現(xiàn)頁(yè)面顯示的錯(cuò)誤信息更改為如下內(nèi)容:

1668657950439_8.jpg

  3.分析問(wèn)題及解決方案

  為什么沒(méi)有顯示"用戶名不存在"的錯(cuò)誤信息呢?經(jīng)過(guò)讀取源碼可發(fā)現(xiàn)如下內(nèi)容,DaoAuthenticationProvider類(lèi)中有如下代碼:

protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            //會(huì)調(diào)用自定義UserService中提供的loadUserByUsername方法,當(dāng)該方法拋出異常后會(huì)被捕獲到
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

  當(dāng)上述方法出現(xiàn)異常后,會(huì)拋給父類(lèi)進(jìn)行捕獲

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));

        // Determine username
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");
                //此處hideUserNotFoundException為true,所以當(dāng)出現(xiàn)UsernameNotFoundException會(huì)被封裝成 Bad Credentials信息
                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }

            Assert.notNull(user,
                    "retrieveUser returned null - a violation of the interface contract");
        }
        //....
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

  上述父類(lèi)的代碼中的hideUserNotFoundExceptions默認(rèn)為true,所以UsernameNotFoundException會(huì)被封裝成BadCredentialsException異常信息,所以需要修改hideUserNotFoundExceptions的值,通過(guò)以下配置文件進(jìn)行設(shè)置,springsecurity.xml修改內(nèi)容如下:

  

  啟動(dòng)運(yùn)行測(cè)試結(jié)果如下:

1668658080660_10.jpg

  但是當(dāng)用戶輸入的密碼錯(cuò)誤依然是"Bad credentials",那該如何解決?

  六、自定義用戶密碼錯(cuò)誤的錯(cuò)誤信息

  1.更改國(guó)際化文件

1668658107127_11.jpg

  默認(rèn)情況下springsecurity會(huì)從messages.properties中取對(duì)應(yīng)的錯(cuò)誤信息,在當(dāng)前包下,會(huì)有一個(gè)對(duì)應(yīng)的messages_zh_CN.properties中有對(duì)應(yīng)的中文錯(cuò)誤信息,該如何更改默認(rèn)獲取錯(cuò)誤的文件?

  課有采用如下方式進(jìn)行修改:

1668658138451_12.jpg

  修改完后,啟動(dòng)測(cè)試,效果如下:

1668658151969_13.jpg

  2.更改顯示內(nèi)容

  上面已經(jīng)能將錯(cuò)誤信息顯示成"壞的憑證"用戶看著不是特別明白,所以可以通過(guò)自定義的方式進(jìn)行錯(cuò)誤信息展示,在自己項(xiàng)目中的resources目錄下新建一個(gè) messages目錄,在messages目錄下創(chuàng)建一個(gè)名字為messages_zh_CN.properties的配置文件,在配置文件中添加如下內(nèi)容:

1668658180374_14.jpg

  并將springsecurity.xml配置文件中將獲取錯(cuò)誤信息的配置文件路徑更改成如下內(nèi)容:

1668658196965_15.jpg

  3.啟動(dòng)服務(wù),運(yùn)行測(cè)試

1668658211430_16.jpg

  至此,對(duì)應(yīng)的錯(cuò)誤信息就已經(jīng)能夠按照我們的意愿進(jìn)行顯示。

  七、總結(jié)

  通過(guò)上述方式即可將錯(cuò)誤信息進(jìn)行自定義展示,當(dāng)前springsecurity默認(rèn)的錯(cuò)誤信息還有非常多,大家可以根據(jù)自己的需要將錯(cuò)誤信息進(jìn)行自定義展示。下面提供一個(gè)springsecurity.xml的完整配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:security="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security.xsd">
    <!--配置放行資源 -->
    <security:http pattern="/login.html" security="none"/>
    <security:http pattern="/js/**" security="none"/>
    <security:http pattern="/login/getErrorMsg.do" security="none"/>
    <!--配置攔截規(guī)則 -->
    <security:http use-expressions="false">
        <security:intercept-url pattern="/**" access="ROLE_ADMIN"/>
        <!--采用默認(rèn)springSecurity提供的默認(rèn)登陸頁(yè)面 -->
        <!--<security:form-login/>-->
        <!--更換自己的登陸頁(yè)面
            login-page:設(shè)定登陸login.html頁(yè)面
            default-target-url:默認(rèn)登錄成功后跳轉(zhuǎn)的url
            authentication-failure-forward-url:登陸失敗后跳轉(zhuǎn)的頁(yè)面
        -->
        <security:form-login login-page="/login.html"
            login-processing-url="/login"
            default-target-url="/index.html"
            authentication-failure-url="/login.html?error=true"/>
        <security:csrf disabled="true"/>
    </security:http>
    <security:authentication-manager>
        <!--<security:authentication-provider user-service-ref="userDetailService">
        </security:authentication-provider>-->
        <security:authentication-provider ref="daoAuthenticationProvider">
        </security:authentication-provider>
    </security:authentication-manager>
    <bean id="daoAuthenticationProvider"
        class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
        <property name="hideUserNotFoundExceptions" value="false"/>
        <property name="userDetailsService" ref="userDetailService"/>
        <property name="messageSource" ref="messageSource"/>
    </bean>
    <bean id="messageSource"
        class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
        <property name="basenames" value="classpath:messages/messages_zh_CN"/>
    </bean>
    <bean id="userDetailService" class="UserDetailService實(shí)現(xiàn)類(lèi)"></bean>
</beans>


分享到:
在線咨詢 我要報(bào)名
和我們?cè)诰€交談!