A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

本帖最后由 大山哥哥 于 2019-3-28 19:08 编辑

        spring security提供了"记住我"的功能,来完成用户下次访问时自动登录功能,而无需再次输入用户名密码。        下面,我们来通过代码演示该功能的实现——主要是通过配置remember-me标签。

        我们通过如下的配置过程来实现“记住我”的功能:
        1、搭建maven项目(web工程),引入功能的相关依赖
        2、配置web.xml中的spring、springsecurity的加载、springmvc的DispatcherServlet
        3、配置spring、springmvc、springsecurity的配置文件
        4、提供UserDetailService接口的实现类,采用写死数据完成认证用户的封装
        5、controller代码提供查询返回当前登录人姓名的功能
        6、准备登录页面login.html以及登录成功页面index.html和登录失败页面failer.html
        7、测试“记住我”


1、搭建maven项目(web工程),引入功能的相关依赖

        maven工程打包方式选择war,同时需要在项目pom做如下配置(只提供主要的依赖和插件,其他的可以参见附件代码):
[XML] 纯文本查看 复制代码
<dependencies>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>5.0.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>5.0.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.0.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.3.RELEASE</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.6.8</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <configuration>
                    <port>80</port>
                </configuration>
                <version>2.2</version>
            </plugin>
        </plugins>
    </build>
2、配置web.xml中的spring、springsecurity的加载、springmvc的DispatcherServlet
[XML] 纯文本查看 复制代码
<!-- 前端控制器(加载classpath:springmvc.xml 服务器启动创建servlet) -->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 配置初始化参数,创建完DispatcherServlet对象,加载springmvc.xml配置文件 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <!-- 服务器启动的时候,让DispatcherServlet对象创建 -->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

还需要在web.xml中配置一个filter——springsecurity安全框架好,这个filter离不了!
[XML] 纯文本查看 复制代码
<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>

3、配置spring、springmvc、springsecurity的配置文件
        ——此处配置省略spring、springmvc的内容,可以参见附件,重点说明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
    [url=http://www.springframework.org/schema/beans/spring-beans.xsd]http://www.springframework.org/schema/beans/spring-beans.xsd[/url]
    [url=http://www.springframework.org/schema/security]http://www.springframework.org/schema/security[/url]
    [url=http://www.springframework.org/schema/security/spring-security.xsd]http://www.springframework.org/s ... spring-security.xsd[/url]">

    <!-- 配置不拦截的资源 -->
    <security:http pattern="/login.html" security="none"/>
    <security:http pattern="/failer.html" security="none"/>

    <security:http auto-config="true" use-expressions="false">
        <!-- 配置具体的拦截的规则 pattern="请求路径的规则" access="访问系统的人,必须有ROLE_ADMIN的角色" -->
        <security:intercept-url pattern="/**" access="ROLE_ADMIN"/>

        <!-- 定义跳转的具体的页面 -->
        <security:form-login
                login-page="/login.html"
                login-processing-url="/login"
                default-target-url="/index.html"
                authentication-failure-url="/failer.html"
                always-use-default-target="true"
        />
        <!--关键点:remember-me配置后,启用RememberMeAuthenticationFilter过滤功能;
            首次登陆时,如果remember-me标记传入,则会将认证成功的信息(用户名、有效期、密码、安全框架提供的key)加密后回写到cookie;
            下次登陆时,请求中会将cookie带过去RememberMeAuthenticationFilter将其解析后,得到其中的用户名,查询用户信息后,再次将查询到的用户信息加密,将加密后的字符串对比cookie中的字符串
            如果一致,设置认证成功;如果不一致,认证失败,抛出异常-->
        <security:remember-me remember-me-parameter="remember-me" user-service-ref="userService"/>

        <!-- 关闭跨域请求 -->
        <security:csrf disabled="true"/>
        <!-- 配置退出 -->
        <security:logout invalidate-session="true" logout-url="/logout" logout-success-url="/login.html"/>

    </security:http>

    <!--为认证管理器配置认证提供者:自定义userDetailService-->
    <security:authentication-manager>
        <security:authentication-provider user-service-ref="userDetailService">
        </security:authentication-provider>
    </security:authentication-manager>

    <bean id="userService" class="com.itheima.security.service.UserDetailServiceImpl"/>

</beans>


4、提供UserDetailService接口的实现类,采用写死数据完成认证用户的封装

        在上述配置文件中,有一
个beanUserDetailServiceImpl,要想完成自定义的用户认证功能,则需要让该类实现UserDetailService接口
[Java] 纯文本查看 复制代码
@Service("userDetailService")
public class UserDetailServiceImpl implements UserDetailsService {
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //此处写死用户名、密码和权限列表
        DBUser dbUser = getDBUserByUsername(username);
        //封装用户权限列表:写死为拥有ROLE_ADMIN角色
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        authorities.add(new SimpleGrantedAuthority("ROLE_" + dbUser.getRole()));
        //将封装好的User返回
        return new User(dbUser.getUsername(),"{noop}"+dbUser.getPassword(),authorities);
    }

    /**
     * 模拟根据用户名查询数据库中的用户数据——此处假设输入的用户名永远正确,密码都是123456
     * @param username
     * @return
     */
    private DBUser getDBUserByUsername(String username){
        return new DBUser(username,"123456","ADMIN");
    }
}

5、controller层提供类,提供查询返回当前登录人姓名的方法
[Java] 纯文本查看 复制代码
@RestController
public class SecurityController {
    @RequestMapping("getUsername")
    public String getUsername(){
        //得到当前认证对象的用户名并返回
        return SecurityContextHolder.getContext().getAuthentication().getName();
    }
}

6、准备登录页面login.html以及登录成功页面index.html和登录失败页面failer.html
       6.1 login.html:
[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
    <!--注意:action="login" method为post-->
    <form action="login" method="post">
        <!--注意:name="username"-->
        用户名:<input name="username" type="text"/><br>
        <!--注意:name="password"-->
        密   码:<input name="password" type="password"/>
        <hr>
        <!--注意:name="remember-me" 与后台remember-me标签配置的remember-me-parameter一致-->
        <input type="checkbox" name="remember-me" value="true"/>
        <input type="submit" value="登录"/>
    </form>
</body>
</html>

        6.2 index.html:
[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>欢迎页面</title>
    <script src="jquery-2.2.3.min.js"></script>
</head>
<body>
    <span id="nameId"></span>济南欢迎您!
    <a href="logout">退出</a>
    <script>
        $(function(){
            //发送异步请求,获取当前登录用户名
            $.ajax({
                url:'getUsername',
                type:"post",
                success:function(data){
                    $("#nameId").html(data);
                }
            })
        })
    </script>
</body>
</html>

        6.3 failer.html
[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>失败页面</title>
</head>
<body>
    欢迎下次再来济南!
</body>
</html>

7、测试“记住我”
        ——第一次访问首页(index.html),会跳转到登录页面,在登录页面记得勾选那个复选框(remember-me) 首次登录.jpg
        ——将浏览器直接关闭,再打开后访问首页(index.html),,会发现,本次访问无需登录即可访问
==================================================================================
下面,我们来简单了解一下在配置文件中添加remember-me元素之后,springsecurity框架做的事情:        

        1.在首次登录时,会经过remember-me对应的过滤器RememberMeAuthenticationFilter在调用到userDetailService获取用户数据并认证成功之后,会经过TokenBasedRememberMeServices的onLoginSuccess方法,如下
[Java] 纯文本查看 复制代码
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        String username = this.retrieveUserName(successfulAuthentication);
        String password = this.retrievePassword(successfulAuthentication);
        if (!StringUtils.hasLength(username)) {
            this.logger.debug("Unable to retrieve username");
        } else {
            if (!StringUtils.hasLength(password)) {
                //调用自定义的userDetailService,得到返回的UserDetail对象
                UserDetails user = this.getUserDetailsService().loadUserByUsername(username);
                password = user.getPassword();
                if (!StringUtils.hasLength(password)) {
                    this.logger.debug("Unable to obtain password for user: " + username);
                    return;
                }
            }

            int tokenLifetime = this.calculateLoginLifetime(request, successfulAuthentication);
            long expiryTime = System.currentTimeMillis();
            expiryTime += 1000L * (long)(tokenLifetime < 0 ? 1209600 : tokenLifetime);
            //将时长、用户名、密码通过md5加密,得到签名字符串
            String signatureValue = this.makeTokenSignature(expiryTime, username, password);
            //新建cookie并将其写入到response中
            this.setCookie(new String[]{username, Long.toString(expiryTime), signatureValue}, tokenLifetime, request, response);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
            }

        }
    }

本次记录的签名值截图如下:
记录1-签名值.jpg

        this.setCookie方法会进一步将编码形成cookieValue
[Java] 纯文本查看 复制代码
protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request, HttpServletResponse response) {
        //将用户名、密码、签名组成的tokens编码后,得到cookieValue
        String cookieValue = this.encodeCookie(tokens);
        Cookie cookie = new Cookie(this.cookieName, cookieValue);
        cookie.setMaxAge(maxAge);
        cookie.setPath(this.getCookiePath(request));
        //...省略部分代码
        response.addCookie(cookie);
    }

保存的cookie值如下:
cookie模样.jpg
也可以在浏览器抓包中看到:
记录2-抓包.jpg

        2.关闭浏览器再次打开时该cookie在存活时间内,会跟着请求带到后台,再次经过RememberMeAuthenticationFilter过滤器,执行doFilter方法
[Java] 纯文本查看 复制代码
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            //autoLogin方法来读取cookie,校验cookie中保存的数据和数据库中查询的是否一致
            Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
            //省略部分代码
            chain.doFilter(request, response);
        }
    }

        在autoLogin方法中进行校验:
[Java] 纯文本查看 复制代码
    public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
        //获取cookieValue
        String rememberMeCookie = this.extractRememberMeCookie(request);
        //省略很多代码
            //将cookieValue进行解析,得到其中的用户名、有效期以及签名
            String[] cookieTokens = this.decodeCookie(rememberMeCookie);
            //校验cookieValue中的签名
            user = this.processAutoLoginCookie(cookieTokens, request, response);
            this.userDetailsChecker.check(user);
            this.logger.debug("Remember-me cookie accepted");
            //经过验证后,将用户写如认证主体中
            return this.createSuccessfulAuthentication(request, user);
        //省略很多代码
            this.cancelCookie(request, response);
            return null;
    }
解析cookieValue之后的cookieTokens内容如下:
记录3-cookie解析后.jpg
        进一步的验证在processAutoLoginCookie方法中进行:
[Java] 纯文本查看 复制代码
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
            //省略部分代码
                //根据解析出来的用户名查询数据库用户
                UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(cookieTokens[0]);
                //将有效期以及查询出的数据库用户用户名、密码再次通过MD5加密,得到新的一个签名(expectedTokenSignature)
                String expectedTokenSignature = this.makeTokenSignature(tokenExpiryTime, userDetails.getUsername(), userDetails.getPassword());
                //判断新的签名和通过cookieVlaue解析出来的签名是否一致
                if (!equals(expectedTokenSignature, cookieTokens[2])) {
                    throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2] + "' but expected '" + expectedTokenSignature + "'");
                } else {
                    //如果一致,返回该用户
                    return userDetails;
                }
    }

        经过加密之后新的签名值截图如下:
记录4-新签名值.jpg
新的签名值和编码到cookieValue中的签名完全一致!

        本次记住我功能配置使用的是较简单——仅通过cookie没有通过数据库记录的——功能实现,源码中挑选了其中几个关键点来加以说明“记住我”功能的实现过程,欢迎更深入的交流。
附件: spring_security_rm.zip (43.17 KB, 下载次数: 112)

0 个回复

您需要登录后才可以回帖 登录 | 加入黑马