ssm中的spring security

1 一个简单的security项目

1.1 依赖

<!--jsp页面的el标签-->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>
<!--security的配置包-->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>

1.2 spring security 几个包

spring-security-core.jar

核心包,任何Spring Security功能都需要此包。

spring-security-web.jar

web工程必备,包含过滤器和相关的Web安全基础结构代码。

spring-security-confifig.jar

用于解析xml配置文件,用到Spring Security的xml配置文件的就要用到此包。

spring-security-taglibs.jar

Spring Security提供的动态标签库,jsp页面可以用。

1.3 配置web.xml

<!--security的过滤器链,名字必须固定为springSecurityFilterChain-->
<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>

1.4 配置spring-security.xml

<?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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
       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/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd
            http://www.springframework.org/schema/security
            http://www.springframework.org/schema/security/spring-security.xsd">

    <!--
    auto-config="true"  使用security的默认配置
    use-expressions="true"  使用jsp页面security的el表达式
    -->
    <security:http auto-config="true" use-expressions="true">
        <!--使用spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>


    </security:http>
    
    
    <!--配置一个用户,这里是模拟,不连数据库-->
    <security:authentication-manager>
        <security:authentication-provider>
            <security:user-service>
                 <!--
					password="{noop}1234"	{noop}表示不使用密码加密功能
				 -->
                <security:user name="lx" authorities="ROLE_USER" password="{noop}1234"/>
            </security:user-service>
        </security:authentication-provider>
    </security:authentication-manager>
</beans>

1.5 使spring能读取到spring-security.xml

在applicationContext.xml(spring 的核心配置文件)配置文件中加入

<!--导入springsecurity的配置文件-->
<import resource="spring-security.xml"></import>

1.6 运行项目

此页面是spring security中自带的页面

在这里插入图片描述

输入账号密码后会成功登录到首页

2 security的过滤器链

2.1 流程

在这里插入图片描述

上面程序启动的时候,会在控制台打印图片内容,你会发现,security加载了一系列的过滤器。

查看源码org.springframework.web.filter.DelegatingFilterProxy,也就是web.xml中的过滤器链

直接找doFilter方法,里面所有判断通过后,会调用一个方法initDelegate

在这里插入图片描述

debug到最后一步,你会发现返回一个FilterChainProxy的过滤器

在这里插入图片描述

进入该类的源码

在这里插入图片描述

进入doFilterInternal方法

在这里插入图片描述

再来看看第二步中的getFilters方法

在这里插入图片描述

最后看SecurityFilterChain,这是个接口,实现类也只有一个DefaultSecurityFilterChain,这才是web.xml中配置的过滤器链对象,也就是控制台上面打印的那个。

2.2 security中的过滤器

访问资源的时候,会经过一个个的过滤器,每个过滤器有自己的功能,只有所有过滤器都验证通过后才能访问到资源

  1. org.springframework.security.web.context.SecurityContextPersistenceFilter

首当其冲的一个过滤器,作用之重要,自不必多言。

SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个

SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。

SecurityContext中存储了当前用户的认证以及权限信息。

  1. org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter

此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager

  1. org.springframework.security.web.header.HeaderWriterFilter

向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制

  1. org.springframework.security.web.csrf.CsrfFilter

csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,

如果不包含,则报错。起到防止csrf攻击的效果。

  1. org.springframework.security.web.authentication.logout.LogoutFilter

匹配URL为/logout的请求,实现用户退出,清除认证信息。

  1. org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。

  1. org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter

如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。

  1. org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter

由此过滤器可以生产一个默认的退出登录页面

  1. org.springframework.security.web.authentication.www.BasicAuthenticationFilter

此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。

  1. org.springframework.security.web.savedrequest.RequestCacheAwareFilter

通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest

  1. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter

针对ServletRequest进行了一次包装,使得request具有更加丰富的API

  1. org.springframework.security.web.authentication.AnonymousAuthenticationFilter

当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。

spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。

  1. org.springframework.security.web.session.SessionManagementFilter

SecurityContextRepository限制同一用户开启多个会话的数量

  1. org.springframework.security.web.access.ExceptionTranslationFilter

异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常

  1. org.springframework.security.web.access.intercept.FilterSecurityInterceptor

获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权

3 security中使用自定义的登录认证页面

3.1 修改spring-security.xml文件

<?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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
       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/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd
            http://www.springframework.org/schema/security
            http://www.springframework.org/schema/security/spring-security.xsd">

    <!--静态资源无需经过security安全控制-->
    <security:http pattern="/css/**" security="none"/>
    <security:http pattern="/img/**" security="none"/>
    <security:http pattern="/plugins/**" security="none"/>
    <security:http pattern="/failer.jsp" security="none"/>

    <!--
    auto-config="true"  使用security的默认配置
    use-expressions="true"  使用jsp页面security的el表达式
    -->
    <security:http auto-config="true" use-expressions="true">
        <!--login.jsp允许匿名访问-->
        <security:intercept-url pattern="/login.jsp" access="permitAll()"/>
        <!--使用spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>

        <!--
        login-page="/login.jsp" 指定自定义的登录页面
        login-processing-url="/login"
        default-target-url="/index.jsp" 指定登录成功后的默认页面
        authentication-failure-url="/failer.jsp" 指定未登录访问资源跳转的页面
        -->
        <security:form-login login-page="/login.jsp"
                             login-processing-url="/login"
                             default-target-url="/index.jsp"
                             authentication-failure-url="/failer.jsp" />

        <!--
        logout-url="/logout" 指定处理登出的controller路径
        logout-success-url="/login.jsp" 指定登出成功后跳转的路径
        -->
        <security:logout logout-url="/logout" logout-success-url="/login.jsp"/>

        <!--禁止开启跨站请求伪造,如果不开启此功能,登录后访问首页仍然会报403错误-->
        <security:csrf disabled="true"/>
    </security:http>

    <!--内存中配置一个用户-->
    <security:authentication-manager>
        <security:authentication-provider>
            <security:user-service>
                <!--
					password="{noop}1234"	{noop}表示不使用密码加密功能
				 -->
                <security:user name="lx" authorities="ROLE_USER" password="{noop}1234"></security:user>
            </security:user-service>
        </security:authentication-provider>
    </security:authentication-manager>
</beans>

3.2 修改login.jsp

处理登录过程的地址是/login

<div class="login-box-body">
    <p class="login-box-msg">登录系统</p>
    <form action="${pageContext.request.contextPath}/login" method="post">
        <div class="form-group has-feedback">
            <input type="text" name="username" class="form-control"
                   placeholder="用户名"> <span
                                            class="glyphicon glyphicon-envelope form-control-feedback"></span>
        </div>
        <div class="form-group has-feedback">
            <input type="password" name="password" class="form-control"
                   placeholder="密码"> <span
                                           class="glyphicon glyphicon-lock form-control-feedback"></span>
        </div>
        <div class="row">
            <div class="col-xs-8">
                <div class="checkbox icheck">
                    <label><input type="checkbox" name="remember-me" value="true"> 记住 下次自动登录</label>
                </div>
            </div>
            <!-- /.col -->
            <div class="col-xs-4">
                <button type="submit" class="btn btn-primary btn-block btn-flat">登录</button>
            </div>
            <!-- /.col -->
        </div>
    </form>

    <a href="#">忘记密码</a><br>


</div>

3.3 修改登出的页面代码

<!-- Menu Footer-->
<li class="user-footer">
    <div class="pull-left">
        <a href="#" class="btn btn-default btn-flat">修改密码</a>
    </div>
    <div class="pull-right">
        <%--security规定注销必须使用post请求--%>
        <form action="${pageContext.request.contextPath}/logout" method="post">
            <button type="submit" class="btn btn-default btn-flat">注销</button>
        </form>
    </div>
</li>

4 security中csrf防护机制

如果我们没有此项配置,登录后访问首页仍会报403(权限不足)

4.1 关闭csrf

只需在spring-security.xml中加入这项配置即可

 <security:csrf disabled="true"/>

4.2 在自定义的登录页面中使用csrf

如果我们要使用这个csrf的话,我们必须在登录的jsp中加入如下内容

<%--使用security的el标签库--%>
<%@taglib prefix="security" uri="http://www.springframework.org/security/tags" %>

在登录的form表单中加入下面内容

<%--会自动产生一个csrf参数--%>
<security:csrfInput/>

启动项目访问登录页面,F12查看源代码

在这里插入图片描述

输入账号密码,可以正常登录了,但是点注销按钮的时候,发现又报了403,这是什么原因呢?

4.2.1 csrf机制的流程

csrf是由security过滤器链中的CsrfFilter提供的,我们进去看一下源码

在这里插入图片描述

这个matches方法到底是干什么的,我们接着找

在这里插入图片描述

我们还发现DefaultRequiresCsrfMatcher他是一个内部类

在这里插入图片描述

如果不是这四个方法类型,我们看doFilterInternal方法开头

在这里插入图片描述

它将csrfToken设置到request上,就是为了我们能在页面上使用security的el标签获取到token,最后在表单提交的时候,将这个token作为请求体中的一个参数发送给服务器

在这里插入图片描述

所以最后的结论就是:它会判断方法的类型,如果是"GET", “HEAD”, “TRACE”, "OPTIONS"之一,直接放行,否则的话,就会匹配表单中的actualToken和和服务器上的csrfToken是否相等,相等就放行,否则在控制台打印Invalid CSRF token found for ....,这样如果别的用户没有拿到对应的csrfToken就不能对系统做任何修改删除操作

5 连接数据库

5.1 取得UserDetails(封装了用户信息)

根据上面的内容我们已经知道了UsernamePasswordAuthenticationFilter这个过滤器是处理认证过程的,我们现在来分析一下

在这里插入图片描述

由此可知,真正的认证过程在AuthenticationManager的对象中,找这个接口的实现类

在这里插入图片描述

进去看下源码,注意一下这个providers

在这里插入图片描述

认证方法

在这里插入图片描述

我们接着去找AuthenticationProvider的实现类DaoAuthenticationProvider,authenticate方法在AbstractUserDetailsAuthenticationProvider中

在这里插入图片描述

DaoAuthenticationProvider重写了retrieveUser方法,现在我们去DaoAuthenticationProvider类

在这里插入图片描述

UserDetails就是SpringSecurity自己的用户对象。

this.getUserDetailsService()其实就是得到UserDetailsService的一个实现类

loadUserByUsername里面就是真正的认证逻辑

也就是说我们可以直接编写一个UserDetailsService的实现类,告诉SpringSecurity就可以了!

loadUserByUsername方法中只需要返回一个UserDetails对象即可

接下来就简单了,我们只需实现这个接口就可以获取我们自己数据库中的用户信息

在这里插入图片描述

5.2 取得UserDetails之后

在AbstractUserDetailsAuthenticationProvider中

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
	//最后一行,看看对他做了啥
	return createSuccessAuthentication(principalToReturn, authentication, user);
}

在这里插入图片描述

我们去UsernamePasswordAuthenticationToken的构造方法看看

/**
 * This constructor should only be used by <code>AuthenticationManager</code> or
 * <code>AuthenticationProvider</code> implementations that are satisfied with
 * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
 * authentication token.
 *
 * @param principal
 * @param credentials
 * @param authorities
 */
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
                                           Collection<? extends GrantedAuthority> authorities) {
    //主要看这里,我们看他对权限做了啥
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    super.setAuthenticated(true); // must use super, as we override
}

进入父类的构造方法

/**
 * Creates a token with the supplied array of authorities.
 *
 * @param authorities the collection of <tt>GrantedAuthority</tt>s for the principal
 *                    represented by this authentication object.
 */
public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
    //权限不能为空
    if (authorities == null) {
        this.authorities = AuthorityUtils.NO_AUTHORITIES;
        return;
    }

    //权限不能为空
    for (GrantedAuthority a : authorities) {
        if (a == null) {
            throw new IllegalArgumentException(
                "Authorities collection cannot contain any null elements");
        }
    }
    //把权限重新放到list集合中
    ArrayList<GrantedAuthority> temp = new ArrayList<>(
        authorities.size());
    temp.addAll(authorities);
    this.authorities = Collections.unmodifiableList(temp);
}

由此,我们需要牢记自定义认证业务逻辑返回的UserDetails对象中一定要放置权限信息

5.3 认证成功后做的操作

现在我们回到UsernamePasswordAuthenticationFilter这个过滤器,我们一直没有找到他的doFilter方法,现在,我们去他父类AbstractAuthenticationProcessingFilter找找。

/**
 * Invokes the
 * {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse)
 * requiresAuthentication} method to determine whether the request is for
 * authentication and should be handled by this filter. If it is an authentication
 * request, the
 * {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse)
 * attemptAuthentication} will be invoked to perform the authentication. There are
 * then three possible outcomes:
 * <ol>
 * <li>An <tt>Authentication</tt> object is returned. The configured
 * {@link SessionAuthenticationStrategy} will be invoked (to handle any
 * session-related behaviour such as creating a new session to protect against
 * session-fixation attacks) followed by the invocation of
 * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)}
 * method</li>
 * <li>An <tt>AuthenticationException</tt> occurs during authentication. The
 * {@link #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException)
 * unsuccessfulAuthentication} method will be invoked</li>
 * <li>Null is returned, indicating that the authentication process is incomplete. The
 * method will then return immediately, assuming that the subclass has done any
 * necessary work (such as redirects) to continue the authentication process. The
 * assumption is that a later request will be received by this method where the
 * returned <tt>Authentication</tt> object is not null.
 * </ol>
 */
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    throws IOException, ServletException {

    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;

    //这里就是判断请求是否是登录认证(/login)请求,如果是其他请求就直接放行
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);

        return;
    }

    if (logger.isDebugEnabled()) {
        logger.debug("Request is to process authentication");
    }

    Authentication authResult;

    try {
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
            // return immediately as subclass has indicated that it hasn't completed
            // authentication
            return;
        }
        sessionStrategy.onAuthentication(authResult, request, response);
    }
    catch (InternalAuthenticationServiceException failed) {
        logger.error(
            "An internal error occurred while trying to authenticate the user.",
            failed);
        //登录失败去了这里,一会我们去看看
        unsuccessfulAuthentication(request, response, failed);

        return;
    }
    catch (AuthenticationException failed) {
        // Authentication failed
        unsuccessfulAuthentication(request, response, failed);

        return;
    }

    // Authentication success
    if (continueChainBeforeSuccessfulAuthentication) {
        chain.doFilter(request, response);
    }

    //登录成功去这里
    successfulAuthentication(request, response, chain, authResult);
}

先看登录成功后的操作

/**
 * Default behaviour for successful authentication.
 * <ol>
 * <li>Sets the successful <tt>Authentication</tt> object on the
 * {@link SecurityContextHolder}</li>
 * <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
 * <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
 * <tt>ApplicationEventPublisher</tt></li>
 * <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
 * </ol>
 *
 * Subclasses can override this method to continue the {@link FilterChain} after
 * successful authentication.
 * @param request
 * @param response
 * @param chain
 * @param authResult the object returned from the <tt>attemptAuthentication</tt>
 * method.
 * @throws IOException
 * @throws ServletException
 */
protected void successfulAuthentication(HttpServletRequest request,
                                        HttpServletResponse response, FilterChain chain, Authentication authResult)
    throws IOException, ServletException {

    if (logger.isDebugEnabled()) {
        logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                     + authResult);
    }

    //认证成功,将认证信息放入securityContext容器中
    SecurityContextHolder.getContext().setAuthentication(authResult);

    //认证成功,使用记住我功能
    rememberMeServices.loginSuccess(request, response, authResult);

    // Fire event
    if (this.eventPublisher != null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
            authResult, this.getClass()));
    }

    successHandler.onAuthenticationSuccess(request, response, authResult);
}

登录失败后的操作

/**
 * Default behaviour for unsuccessful authentication.
 * <ol>
 * <li>Clears the {@link SecurityContextHolder}</li>
 * <li>Stores the exception in the session (if it exists or
 * <tt>allowSesssionCreation</tt> is set to <tt>true</tt>)</li>
 * <li>Informs the configured <tt>RememberMeServices</tt> of the failed login</li>
 * <li>Delegates additional behaviour to the {@link AuthenticationFailureHandler}.</li>
 * </ol>
 */
protected void unsuccessfulAuthentication(HttpServletRequest request,
                                          HttpServletResponse response, AuthenticationException failed)
    throws IOException, ServletException {
    SecurityContextHolder.clearContext();

    if (logger.isDebugEnabled()) {
        logger.debug("Authentication request failed: " + failed.toString(), failed);
        logger.debug("Updated SecurityContextHolder to contain null Authentication");
        logger.debug("Delegating to authentication failure handler " + failureHandler);
    }

    //记住我  失败
    rememberMeServices.loginFail(request, response);

    failureHandler.onAuthenticationFailure(request, response, failed);
}

可见AbstractAuthenticationProcessingFilter这个过滤器对于认证成功与否,做了两个分支,成功执行

successfulAuthentication,失败执行unsuccessfulAuthentication。

在successfulAuthentication内部,将认证信息存储到了SecurityContext中。并调用了loginSuccess方法,这就是

常见的“记住我”功能!

5.4 初步实现认证功能

5.4.1 使自己的UserService接口继承UserDetailsService
//继承接口
public interface UserService extends UserDetailsService {

}
5.4.2 实现loadUserByUsername方法
@Service
@Transactional
public class UserServiceImpl implements UserService {
    
    @Autowired
    private UserDao userDao;


    /**
     * 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
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名获取用户对象
        SysUser sysUser = userDao.findByName(username);
        if (null == sysUser) {
            //不存在直接返回空,security会自动对它处理
            return null;
        }
        
  
         /**
         * UserDetails是一个抽象类,我们找它的实现类User,它有两个构造方法,我们使用简单的那一个
         * username 用户名
         * password 密码
         * Collection<? extends GrantedAuthority> authorities   集合,里面的元素为GrantedAuthority的实现类,这里我们用SimpleGrantedAuthority
         */
        List<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();
        for (SysRole role : sysUser.getRoles()) {
            authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
        }
        UserDetails userDetails = new User(sysUser.getUsername(),
                "{noop}" + sysUser.getPassword(),
                authorities);
        return userDetails;
    }
}

5.4.3 配置spring-security.xml使自己重写的loadUserByUsername生效
<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
    <!--
        user-service-ref="userServiceImpl" 指定使用这个对象去数据库加载用户信息
        对象名字就是类名首字母小写
        -->
    <security:authentication-provider user-service-ref="userServiceImpl"></security:authentication-provider>
</security:authentication-manager>
5.4.4 测试

测试的时候必须给测试的用户配置角色,否则仍然会登录失败

5.5 使用加密认证

5.5.1 修改spring-security.xml配置文件
<!--加密对象-->
<bean id="bCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
    <!--
        user-service-ref="userServiceImpl" 指定使用这个对象去数据库加载用户信息
        对象名字就是类名首字母小写
        -->
    <security:authentication-provider user-service-ref="userServiceImpl">
        <security:password-encoder ref="bCryptPasswordEncoder"/>
    </security:authentication-provider>
</security:authentication-manager>
5.5.2 修改loadUserByUsername方法,去掉“{noop}”
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

    SysUser sysUser = userDao.findByName(username);
    if (null == sysUser) {
        return null;
    }
    /**
         * username 用户名
         * password 密码
         * Collection<? extends GrantedAuthority> authorities   集合,里面的元素为GrantedAuthority的实现类,这里我们用SimpleGrantedAuthority
         */
    List<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();
    for (SysRole role : sysUser.getRoles()) {
        authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
    }
    UserDetails userDetails = new User(sysUser.getUsername(),
                                       //去掉“{noop}”
                                       sysUser.getPassword(),
                                       authorities);
    return userDetails;
}
5.5.3 测试
public static void main(String[] args) {
        System.out.println(new BCryptPasswordEncoder().encode("123"));
}

直接修改数据库

5.6 使用用户状态

我们在loadUserByUsername方法中构造UserDetails的对象的时候,使用的是三个参数的User构造方法,其实他还有一个构造方法,这个构造方法中就加入了用户状态

5.6.1 User的第二个构造方法
/**
 * Construct the <code>User</code> with the details required by
 * {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider}.
 *
 * @param username the username presented to the
 * <code>DaoAuthenticationProvider</code>
 * @param password the password that should be presented to the
 * <code>DaoAuthenticationProvider</code>
 * @param enabled set to <code>true</code> if the user is enabled
 * @param accountNonExpired set to <code>true</code> if the account has not expired
 * @param credentialsNonExpired set to <code>true</code> if the credentials have not
 * expired
 * @param accountNonLocked set to <code>true</code> if the account is not locked
 * @param authorities the authorities that should be granted to the caller if they
 * presented the correct username and password and the user is enabled. Not null.
 *
 * @throws IllegalArgumentException if a <code>null</code> value was passed either as
 * a parameter or as an element in the <code>GrantedAuthority</code> collection
 */
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)) {
        throw new IllegalArgumentException(
            "Cannot pass null or empty values to constructor");
    }

    this.username = username;
    this.password = password;
    this.enabled = enabled;
    this.accountNonExpired = accountNonExpired;
    this.credentialsNonExpired = credentialsNonExpired;
    this.accountNonLocked = accountNonLocked;
    this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}

下面是几个布尔参数的解释,也可以看方法上的注释,还是很简单的

boolean enabled 是否可用

boolean accountNonExpired 账户是否失效

boolean credentialsNonExpired 密码是否失效

boolean accountNonLocked 账户是否锁定

5.6.2 判断用户的认证状态
UserDetails userDetails = new User(sysUser.getUsername(),
                sysUser.getPassword(),
                //根据业务的需要选择使用,这里我们只使用一个
                //status的值为1的时候,启用该账号
                sysUser.getStatus()==1,
                true,
                true,
                true,
                authorities);

6 remember me

6.1 记住我功能原理分析

从AbstractAuthenticationProcessingFilter类中找到doFilter方法,我们发现认证成功后走的方法是

/**
 * Default behaviour for successful authentication.
 * <ol>
 * <li>Sets the successful <tt>Authentication</tt> object on the
 * {@link SecurityContextHolder}</li>
 * <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
 * <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
 * <tt>ApplicationEventPublisher</tt></li>
 * <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
 * </ol>
 *
 * Subclasses can override this method to continue the {@link FilterChain} after
 * successful authentication.
 * @param request
 * @param response
 * @param chain
 * @param authResult the object returned from the <tt>attemptAuthentication</tt>
 * method.
 * @throws IOException
 * @throws ServletException
 */
protected void successfulAuthentication(HttpServletRequest request,
                                        HttpServletResponse response, FilterChain chain, Authentication authResult)
    throws IOException, ServletException {

    if (logger.isDebugEnabled()) {
        logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                     + authResult);
    }

    SecurityContextHolder.getContext().setAuthentication(authResult);

    //remember me功能的代码
    rememberMeServices.loginSuccess(request, response, authResult);

    // Fire event
    if (this.eventPublisher != null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
            authResult, this.getClass()));
    }

    successHandler.onAuthenticationSuccess(request, response, authResult);
}

我们发现rememberMeServices是NullRememberMeServices的实例对象

private RememberMeServices rememberMeServices = new NullRememberMeServices();

进入loginSuccess这个方法看看,发现方法为空。我们使用debug打断点

在这里插入图片描述

发现确实是NullRememberMeServices的实例对象,为什么会这样,原来是我们在登录的时候没有使用记住我的这个功能,现在我我们开启这个功能

6.2 使用remember me

6.2.1 修改登录的表单

所以我们现在去修改页面,开启remember me

<%--登录表单中添加这个--%>
<input type="checkbox" name="remember-me" value="true">
6.2.2 修改spring-security.xml

security默认是不开启remember me这个功能的

<!--
    auto-config="true"  使用security的默认配置
    use-expressions="true"  使用jsp页面security的el表达式
    -->
<security:http auto-config="true" use-expressions="true">
    <!--login.jsp允许匿名访问-->
    <security:intercept-url pattern="/login.jsp" access="permitAll()"/>
    <!--使用spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
    <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>

    <!--
        login-page="/login.jsp" 指定自定义的登录页面
        login-processing-url="/login"
        default-target-url="/index.jsp" 指定登录成功后的默认页面
        authentication-failure-url="/failer.jsp" 指定未登录访问资源跳转的页面
        -->
    <security:form-login login-page="/login.jsp"
                         login-processing-url="/login"
                         default-target-url="/index.jsp"
                         authentication-failure-url="/failer.jsp" />

    <!--
        logout-url="/logout" 指定处理登出的controller路径
        logout-success-url="/login.jsp" 指定登出成功后跳转的路径
        -->
    <security:logout logout-url="/logout" logout-success-url="/login.jsp"/>

    <!--禁止开启跨站请求伪造,如果不开启此功能,登录后访问首页仍然会报403错误-->
    <security:csrf disabled="true"/>

    <!--
		开启remember me过滤器
        token-validity-seconds="60" 设置token存储时间为60秒
        -->
    <security:remember-me token-validity-seconds="60"/>

</security:http>

RememberMeAuthenticationFilter中功能非常简单,会在打开浏览器时,自动判断是否认证,如果没有则

调用autoLogin进行自动认证

6.2.3 测试

先测试一下,认证通过后,关掉浏览器,再次打开页面,不需要认证了!

现在我们接着去debug,看一下rememberMeServices的对象到底是什么

在这里插入图片描述

好了,现在我们知道remember me这个功能主要由这个类完成

6.3 接着说原理

进入TokenBasedRememberMeServices的loginSuccess方法(在父类中)

/**
 * {@inheritDoc}
 *
 * <p>
 * Examines the incoming request and checks for the presence of the configured
 * "remember me" parameter. If it's present, or if <tt>alwaysRemember</tt> is set to
 * true, calls <tt>onLoginSucces</tt>.
 * </p>
 */
@Override
public final void loginSuccess(HttpServletRequest request,
                               HttpServletResponse response, Authentication successfulAuthentication) {

    //这里判断是否勾选记住我
    //这里this.parameter点进去是上面的private String parameter = "remember-me";
    if (!rememberMeRequested(request, parameter)) {
        logger.debug("Remember-me login not requested.");
        return;
    }

    //若勾选就调用onLoginSuccess方法
    onLoginSuccess(request, response, successfulAuthentication);
}

再去看看这个rememberMeRequested,方法很简单

/**
 * Allows customization of whether a remember-me login has been requested. The default
 * is to return true if <tt>alwaysRemember</tt> is set or the configured parameter
 * name has been included in the request and is set to the value "true".
 *
 * @param request the request submitted from an interactive login, which may include
 * additional information indicating that a persistent login is desired.
 * @param parameter the configured remember-me parameter name.
 *
 * @return true if the request includes information indicating that a persistent login
 * has been requested.
 */
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
    if (alwaysRemember) {
        return true;
    }

    //从request请求中获取remember-me的值
    String paramValue = request.getParameter(parameter);

    //若要开启这个功能,其对应值必须是true,on,yes,1
    if (paramValue != null) {
        if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
            || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
            return true;
        }
    }

    if (logger.isDebugEnabled()) {
        logger.debug("Did not send remember-me cookie (principal did not set parameter '"
                     + parameter + "')");
    }

    return false;
}

如果上面方法返回true,就表示页面勾选了记住我选项了。

再去看看这个loginSuccess方法最后一行调用的onLoginSuccess的方法(TokenBasedRememberMeServices中)

@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
                           Authentication successfulAuthentication) {

    String username = retrieveUserName(successfulAuthentication);
    String password = retrievePassword(successfulAuthentication);

    // If unable to find a username and password, just abort as
    // TokenBasedRememberMeServices is
    // unable to construct a valid token in this case.
    if (!StringUtils.hasLength(username)) {
        logger.debug("Unable to retrieve username");
        return;
    }

    if (!StringUtils.hasLength(password)) {
        //从数据库中获取用户信息
        UserDetails user = getUserDetailsService().loadUserByUsername(username);
        password = user.getPassword();

        if (!StringUtils.hasLength(password)) {
            logger.debug("Unable to obtain password for user: " + username);
            return;
        }
    }

    //获取token最大存活时间,前面在配置文件中设置的60
    int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
    //当前系统时间(毫秒数)
    long expiryTime = System.currentTimeMillis();
    // SEC-949
    //到期时间
    expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime);

    //生成签名(签名中包含了用户名和密码信息)
    String signatureValue = makeTokenSignature(expiryTime, username, password);

    //根据签名生成了cookie,并返回
    setCookie(new String[] { username, Long.toString(expiryTime), signatureValue },
              tokenLifetime, request, response);

    if (logger.isDebugEnabled()) {
        logger.debug("Added remember-me cookie for user '" + username
                     + "', expiry: '" + new Date(expiryTime) + "'");
    }
}

在浏览器中会有一个名字为remember-me的cookie

在这里插入图片描述

原理到这里就说的差不多了

6.4 持久化remember me信息

记住我功能是很方便,但是安全性却令人担忧。因为Cookie毕竟是保存在客户端的,很容易盗取,而且 cookie的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,还是不太安全。于是,SpringSecurity就提供了remember me的另一种相对更安全的实现机制 :在客户端的cookie中,仅保存一个无意义的加密串(与用户名、密码等敏感数据无关),然后在数据库中保存该加密串-用户信息的对应关系,自动登录 时,用cookie中的加密串,到数据库中验证,如果通过,自动登录才算通过。

6.4.1 建表

这个表示固定的,不要修改它的结构

CREATE TABLE `persistent_logins` (
   `username` varchar(64) NOT NULL,
   `series` varchar(64) NOT NULL,
   `token` varchar(64) NOT NULL,
   `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
   PRIMARY KEY (`series`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8
6.4.2 修改spring-security.xml
<security:http auto-config="true" use-expressions="true">
        
    <!--省略上面的内容-->

    <!--
        开启remember me过滤器
        token-validity-seconds="60" 设置token存储时间为60秒
        data-source-ref="dataSource"    指定数据源,因为需要将用户信息保存到数据库
        remember-me-parameter="remember-me" 指定记住的参数名
        -->
    <security:remember-me token-validity-seconds="60" data-source-ref="dataSource" remember-me-parameter="remember-me" />

</security:http>
6.4.3 测试

勾选记住我登录之后

数据库中

在这里插入图片描述

浏览器中

在这里插入图片描述

6.4.4 原理

debug模式下,仍然对rememberMeServices.loginSuccess(request, response, authResult);这行打断点,然后勾选记住我登录。

在这里插入图片描述

我们发现这个rememberMeServices又换了一种类型,好,我们接着按照6.3的过一遍

那我们进入PersistentTokenBasedRememberMeServices的loginSuccess方法(在父类AbstractRememberMeServices中,这两父类一样,那loginSuccess方法也就是一样的罗,rememberMeRequested方法也是一样的,唯一区别就是onLoginSuccess方法了)

我们看一下PersistentTokenBasedRememberMeServices的onLoginSuccess方法是咋样的

/**
 * Creates a new persistent login token with a new series number, stores the data in
 * the persistent token repository and adds the corresponding cookie to the response.
 *
 */
protected void onLoginSuccess(HttpServletRequest request,
                              HttpServletResponse response, Authentication successfulAuthentication) {
    String username = successfulAuthentication.getName();

    logger.debug("Creating new persistent login for user " + username);

    PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
        username, generateSeriesData(), generateTokenData(), new Date());
    try {
        //持久化到数据库
        tokenRepository.createNewToken(persistentToken);
        //生成对应的cookie
        addCookie(persistentToken, request, response);
    }
    catch (Exception e) {
        logger.error("Failed to save persistent token ", e);
    }
}

7 权限控制

SpringSecurity可以通过注解的方式来控制类或者方法的访问权限。注解需要对应的注解支持,若注解放在 controller类中,对应注解支持应该放在mvc配置文件中,因为controller类由mvc配置文件扫描并创建的,同理,注解放在service类中,对应注解支持应该放在spring配置文件中。

7.1 注解支持放在mvc的配置文件中

<?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:context="http://www.springframework.org/schema/context"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        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/context
			    http://www.springframework.org/schema/context/spring-context.xsd
			    http://www.springframework.org/schema/aop
			    http://www.springframework.org/schema/aop/spring-aop.xsd
			    http://www.springframework.org/schema/tx
			    http://www.springframework.org/schema/tx/spring-tx.xsd
			    http://www.springframework.org/schema/mvc
			    http://www.springframework.org/schema/mvc/spring-mvc.xsd
                http://www.springframework.org/schema/security
			    http://www.springframework.org/schema/security/spring-security.xsd">

    <context:component-scan base-package="com.itheima.controller"/>

    <mvc:annotation-driven/>

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/pages/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <mvc:default-servlet-handler/>

    <!--
    jsr250-annotations="enabled" java自带的 表示支持jsr250-api的注解,需要jsr250-api的jar包
    pre-post-annotations="enabled"  spring带的注解
    secured-annotations="enabled"   security带的注解
    都是权限相关的注解,我们只需要使用其中一种就可以了,这里我们都开启
    -->
    <security:global-method-security jsr250-annotations="enabled" secured-annotations="enabled" pre-post-annotations="enabled"/>


</beans>

7.2 三类注解

@RolesAllowed({"ROLE_ADMIN","ROLE_PRODUCT"})//JSR-250注解

@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_PRODUCT')")//spring表达式注解

@Secured({"ROLE_ADMIN","ROLE_PRODUCT"})//SpringSecurity注解

8 权限不足异常处理

系统的403页面太难看了,对用户不友好,我们对它做下美化

8.1 编写一个异常处理器

@ControllerAdvice
public class ExceptionAdvice {

    //403异常就跳转到这个页面
    @ExceptionHandler(AccessDeniedException.class)
    public String handleAccessDeniedException(){
        return "forward:/403.jsp";
    }
}

这个类必须要放到controller中,只有这样注解才能被mvc扫描到

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值