使用自定义的认证页面

xml配置

首先在SpringSecurity的配置文件中配置认证信息,包括释放静态资源,让认证页面可以被访问,配置认证信息以及退出设置
释放静态资源

<!-- 释放静态资源 -->
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>

让认证页面可以被访问
这个需要在一开始的<security:http>标签中设置

<!-- 让认证页面可以被匿名访问 -->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>

配置认证信息
也是在<security:http>标签中设置

<!-- 配置认证信息 -->
<!--
    login-page: 登录页面
    login-processing-url:登录处理器
    default-target-url:登录成功后跳转的地址
    authentication-failure-url:登录失败跳转的地址
    username-parameter: 登录表单的name属性值
-->
<security:form-login login-page="/login.jsp"
                     login-processing-url="/login"
                     default-target-url="/index.jsp"
                     authentication-failure-url="/failer.jsp"
                     username-parameter="username"
                     password-parameter="password"/>

退出设置

<!-- 配置退出设置 -->
<security:logout logout-url="/logout"
                 logout-success-url="/login.jsp"/>

最终配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:security="http://www.springframework.org/schema/security"
       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">
    <!--
        auto-config="true": 表示自动加载spring-security的配置文件
        use-expression="true": 表示使用spring的el表达式配置SpringSecurity
    -->
    <!-- 释放静态资源 -->
    <security:http pattern="/css/**" security="none"/>
    <security:http pattern="/img/**" security="none"/>
    <security:http pattern="/plugins/**" security="none"/>

    <security:http auto-config="true" use-expressions="true">
        <!-- 让认证页面可以被匿名访问 -->
        <security:intercept-url pattern="/login.jsp" access="permitAll()"/>

        <!-- 拦截资源 -->
        <!--
            pattern="/**":表示拦截所有的资源
            access="hasAnyRole('ROLE_USER')":表示只有ROLE_USER角色才能访问资源
        -->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>

        <!-- 配置认证信息 -->
        <!--
            login-page: 登录页面
            login-processing-url:登录处理器
            default-target-url:登录成功后跳转的地址
            authentication-failure-url:登录失败跳转的地址
            username-parameter: 登录表单的name属性值
        -->
        <security:form-login login-page="/login.jsp"
                             login-processing-url="/login"
                             default-target-url="/index.jsp"
                             authentication-failure-url="/failer.jsp"
                             username-parameter="username"
                             password-parameter="password"/>

        <!-- 配置退出设置 -->
        <security:logout logout-url="/logout"
                         logout-success-url="/login.jsp"/>
    </security:http>


    <!--设置Spring Security认证用户信息的来源-->
    <security:authentication-manager>
        <security:authentication-provider>
            <security:user-service>
                <!--
                    创建了两个角色
                    springSecurity中的密码是加密的,如果想使用不加密的就要加上{noop}
                -->
                <security:user name="user" password="{noop}user" authorities="ROLE_USER"/>
                <security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN"/>
            </security:user-service>
        </security:authentication-provider>
    </security:authentication-manager>
</beans>

修改请求页面的认证地址

配置完成后,需要将前端表单的请求地址设置为xml中配置的请求地址

springsecurity登录成功后为匿名用户_xml

csrf防护机制

来到登录页面,输入账户密码后点击登录,迎来的并不是登录成功的提示,而是403错误

springsecurity登录成功后为匿名用户_xml_02


解决这个错误的办法有两种,一种是禁用csrf过滤器

禁用csrf过滤器

同样在SpringSecurity中的<security:http>中进行配置即可

<!-- 禁用csrf过滤器 -->
<security:csrf disabled="true"/>

在认证页面携带token请求

上面提到有两种办法解决,那么第二种就是携带csrf过滤器需要的认证token过去
至于为什么使用token就能够解决问题,这个答案可以在csrfFilter的源码中得到

public final class CsrfFilter extends OncePerRequestFilter {
    public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfFilter.DefaultRequiresCsrfMatcher();
    private final Log logger = LogFactory.getLog(this.getClass());
    private final CsrfTokenRepository tokenRepository;
    private RequestMatcher requireCsrfProtectionMatcher;
    private AccessDeniedHandler accessDeniedHandler;

    public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
        this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
        this.accessDeniedHandler = new AccessDeniedHandlerImpl();
        Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
        this.tokenRepository = csrfTokenRepository;
    }

    //通过这里可以看出SpringSecurity的csrf机制把请求方式分成两类来处理
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        boolean missingToken = csrfToken == null;
        if (missingToken) {
            csrfToken = this.tokenRepository.generateToken(request);
            this.tokenRepository.saveToken(csrfToken, request, response);
        }
        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);
        //第一类:"GET", "HEAD", "TRACE", "OPTIONS"四类请求可以直接通过
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            filterChain.doFilter(request, response);
        } else {
            //第二类:除去上面四类,包括POST都要被验证携带token才能通过
            String actualToken = request.getHeader(csrfToken.getHeaderName());
            if (actualToken == null) {
                actualToken = request.getParameter(csrfToken.getParameterName());
            }
            if (!csrfToken.getToken().equals(actualToken)) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
                }
                if (missingToken) {
                    this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
                } else {
                    this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
                }
            } else {
                filterChain.doFilter(request, response);
            }
        }
    }

    public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
        Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null");
        this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
    }

    public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
        Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
        this.accessDeniedHandler = accessDeniedHandler;
    }

    private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
        private final HashSet<String> allowedMethods;

        private DefaultRequiresCsrfMatcher() {
            this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
        }

        public boolean matches(HttpServletRequest request) {
            return !this.allowedMethods.contains(request.getMethod());
        }
    }
}

看完源码就可以很清楚的知道,对于post请求,是一定要携带token的,那么携带token的方式也很简单,如下

springsecurity登录成功后为匿名用户_静态资源_03


只需要在form表单中添加<security:csrfInput/>即可

登出配置

登出操作必须是post请求,因此可以使用下面的方式进行注销

<form action="${pageContext.request.contextPath}/logout" method="post">
    <security:csrfInput/>
    <input type="submit" value="注销">
</form>

初步实现认证功能

继承UserDetailsService接口

通过源码的阅读可知,SpringSecurity会使用UserDetailsService类中的方法来加载用户信息,因此首先需要将我们自己的UserServiceUserDetailsService关联起来,让我们自己的UserService接口继承UserDetailsService接口即可

import org.springframework.security.core.userdetails.UserDetailsService;

public interface UserService extends UserDetailsService {
}

实现接口

然后实现其实现类,并在实现类中实现loadUserByUsername方法

package com.itheima.service.impl;

import com.itheima.domain.SysRole;
import com.itheima.domain.SysUser;
import com.itheima.service.UserService;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Service
@Transactional
public class UserServiceImpl implements UserService {

    /**
     * 写自己的获取用户的业务
     *
     * @param username 用户在浏览器输入的用户名
     * @return UserDetails是SpringSecurity自己的用户对象
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {

            // 根据用户名去数据库做查询
            // SysUser是我们自己的用户实体类对象
            SysUser sysUser = userDao.findByName(username);
            if (null == sysUser) {
                // 没有该用户也是认证失败
                return null;
            }
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            // 添加角色
            List<SysRole> roles = sysUser.getRoles();
            for (SysRole role : roles) {
                authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
            }
            // 构建SpringSecurity认可的用户对象  添加{noop}让SpringSecurity认为该密码就是原文
            UserDetails userDetails = new User(sysUser.getUsername(), "{noop}" + sysUser.getPassword(), authorities);
            return userDetails;
        } catch (Exception e) {
            e.printStackTrace();
            // SpringSecurity内部认为return null 就是认证失败
            return null;
        }
    }
}

在SpringSecurity主配置文件中指定认证使用的业务对象

这一步需要在springSecurity配置文件中将刚刚的实现类与之关联,由于实现类已经被放入了IOC容器中,因此写上id就行,这里没有id,类名首字母小写即可

<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
    <security:authentication-provider user-service-ref="userServiceImpl">
    </security:authentication-provider>
</security:authentication-manager>

最后刷新,再使用数据库的账户密码就可以进行登录了

加密认证

认证基本流程已经做完了,但是还有一些细节需要完善,例如现在的数据库以及认证时的密码都是原文的,这样显然是不安全的,因此后续需要进行加密认证
关于加密,SpringSecurity已经提供了加密对象BCryptPasswordEncoder,接下来就是加密的步骤

在IOC容器中提供加密对象

<!-- 把加密对象放到ioc容器中 -->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
    <security:authentication-provider user-service-ref="userServiceImpl">
        <!--指定认证使用的加密对象-->
        <security:password-encoder ref="passwordEncoder"/>
    </security:authentication-provider>
</security:authentication-manager>

修改认证方法

其实就是去掉之前自定义的loadUserByUsername方法中密码前面的{noop}

修改添加用户的操作

完成上面的步骤就已经可以实现加密认证了,但是还有一个问题,那就是在添加用户的时候需要先将密码加密然后添加

@Service
@Transactional
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;
    @Autowired
    private RoleService roleService;
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    public void save(SysUser user) {
        //对密码进行加密,然后再入库 
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        userDao.save(user);
    }

再次添加用户,密码就是加密后的密文了

设置用户状态

源码分析

用户认证业务里,在new User对象时,选择了三个构造参数的构造方法,其实还有另一个构造方法:

public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
    if (username != null && !"".equals(username) && password != null) {
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.credentialsNonExpired = credentialsNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
    } else {
        throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
    }
}

可以看到,这个构造方法里多了四个布尔类型的构造参数,其实先前使用的三个构造参数的构造方法里这四个布尔
值默认都被赋值为了true,那么这四个布尔值到底是何意思呢?

  • boolean enabled 是否可用
  • boolean accountNonExpired 账户是否失效
  • boolean credentialsNonExpired 秘密是否失效
  • boolean accountNonLocked 账户是否锁定

判断认证用户的状态

这四个参数必须同时为true认证才可以,为了节省时间,只用第一个布尔值做个测试,修改认证业务代码:

/**
 * 写自己的获取用户的业务
 *
 * @param username 用户在浏览器输入的用户名
 * @return UserDetails是SpringSecurity自己的用户对象
 * @throws UsernameNotFoundException
 */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    try {

        // 根据用户名去数据库做查询
        // SysUser是我们自己的用户实体类对象
        SysUser sysUser = userDao.findByName(username);
        if (null == sysUser) {
            // 没有该用户也是认证失败
            return null;
        }
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        // 添加角色
        List<SysRole> roles = sysUser.getRoles();
        for (SysRole role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
        }
        // 构建SpringSecurity认可的用户对象  添加{noop}让SpringSecurity认为该密码就是原文
        UserDetails userDetails = new User(sysUser.getUsername(),
                                        sysUser.getPassword(),
                                        sysUser.getStatus() == 1,
                                        true,
                                        true,
                                        true,
                                        authorities);
        return userDetails;
    } catch (Exception e) {
        e.printStackTrace();
        // SpringSecurity内部认为return null 就是认证失败
        return null;
    }
}

此刻,只有用户状态为1的用户才能成功通过认证!

记住我功能实现

记住我功能页面代码

要实现记住我功能,首先需要前端准备一个单选框或者复选框,name为remember-me,value为true/on/yes/1等都可以

<input type="checkbox" name="remember-me" value="true">

开启remember me过滤器

<!-- 开启remember me过滤器 -->
<security:remember-me token-validity-seconds="60"/>

然后重启项目,就可以使用rememberMe功能了
说明:RememberMeAuthenticationFilter中功能非常简单,会在打开浏览器时,自动判断是否认证,如果没有则
调用autoLogin进行自动认证。

remember me安全性分析

记住我功能虽然很方便,但是问题在于它是将认证的token信息保存在浏览器的cookie中的,这样做是非常不安全的,因为别人可以轻易复制你的token信息去别的浏览器登录。
因此,SpringSecurity还提供了remember me的另一种相对更安全的实现机制 :在客户端的cookie中,仅保存一个
无意义的加密串(与用户名、密码等敏感数据无关),然后在数据库中保存该加密串-用户信息的对应关系,自动登录
时,用cookie中的加密串,到数据库中验证,如果通过,自动登录才算通过。

持久化remember me信息

创建一张表,注意这张表的名称和字段都是固定的,不要修改。

CREATE TABLE `persistent_logins` ( 
	`username` VARCHAR(64) NOT NULL, 
	`series` VARCHAR(64) NOT NULL, 
	`token` VARCHAR(64) NOT NULL, 
	`last_used` TIMESTAMP NOT NULL, PRIMARY KEY (`series`) 
) ENGINE=INNODB DEFAULT CHARSET=utf8

然后修改配置文件中remember-me过滤器相关的配置

<!--
    开启remember me过滤器,
    data-source-ref="dataSource" 指定数据库连接池
    token-validity-seconds="60" 设置token存储时间为60秒 可省略
    remember-me-parameter="remember-me" 指定记住的参数名 可省略
-->
<security:remember-me data-source-ref="dataSource"
                      token-validity-seconds="60"
                      remember-me-parameter="remember-me"/>

上面的dataSource是我们自己配置的数据库连接池的id

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql:///security_authority"/>
    <property name="username" value="root"/>
    <property name="password" value="root"/>
</bean>

重启项目完成后,再次记住我进行登录后,就可以在数据库中查询到相应的token信息,这时的token信息是保存在数据库中的,就会相对安全一些。

springsecurity登录成功后为匿名用户_xml_04

显示当前认证的用户名

在页面中使用下面两个标签都可以显示当前认证的用户名称

<security:authentication property="principal.username"/>
<security:authentication property="name"/>