Spring Security详解

第一节 Spring Security 简介

Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。

  • 用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。

  • 用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

第二节 Spring Security 核心组件

Spring Security详解

 

1. Authentication 认证

//这个接口的实现类都是Token,称为票据,UsernamePasswordAuthenticationToken
//我们自定义实现就可以叫MobileCodeAuthenticationToken
public interface Authentication extends Principal, Serializable {
​
    /**
     * 权限列表
     */
    Collection getAuthorities();
​
    /**
     * 认证凭据,可能是密码,也可能是验证码,也可能是其他认证凭据
     */
    Object getCredentials();
​
    /**
     * 认证请求的详细信息,可能是IP地址,也可能是认证序列号,也可能是null
     */
    Object getDetails();
​
    /**
     * 如果通过认证,则返回的是包含(用户名和密码)或者(手机号和验证码)等的对象;如果认证不通过,
     * 则返回的是用户名或者手机号等。
     */
    Object getPrincipal();
​
    /**
     * 是否认证通过
     */
    boolean isAuthenticated();
​
    /**
     * 修改认证状态
     */
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

Authentication接口就是用来携带认证信息的。认证信息包括用户身份信息,密码,及权限列表等

2. UsernamePasswordAuthenticationFilter

账号密码认证过滤器,用于认证用户信息,认证方式是由 AuthenticationManager 接口提供。

3. AuthenticationManager

public interface AuthenticationManager {
​
   /**
    * 尝试认证传递过来的认证信息,如果认证成功,则会修改认证信息的状态。否则,则会抛出异常
    */
   Authentication authenticate(Authentication authentication) throws AuthenticationException;
​
}

认证管理器,是认证相关的核心接口,也是发起认证的出发点。实际业务中可能根据不同的信息进行认证,所以Spring推荐通过实现 AuthenticationManager 接口来自定义自己的认证方式。Spring 提供了一个默认的实现 ProviderManager。

4. ProviderManager

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    //省略其他属性
    private List providers = Collections.emptyList();
    //省略其他内容
}

认证提供者管理器,该类中维护了一个认证提供者列表,只要这个列表中的任何一个认证提供者提供的认证方式认证通过,认证就结束。

5. AuthenticationProvider

public interface AuthenticationProvider {
​
    /**
     * 执行认证,并返回认证结果
     */
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
​
    /**
     * 支持认证的类型,用于实现自定义认证,比如手机号和短信登录认证需要用户自己来实现
     */
    boolean supports(Class authentication);
}

认证提供者,这是一个接口,具体如何认证,就看如何实现该接口。Spring Security 提供了 DaoAuthenticationProvider 实现该接口,这个类就是使用数据库中数据进行认证。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    //省略其他属性
    private PasswordEncoder passwordEncoder; //密码加密器,主要用于密码加密
    private UserDetailsService userDetailsService; //用户详细信息服务,主要用于查询认证用户信息
    //省略其他内容
}

6. UserDetailsService

public interface UserDetailsService {
​
   /**
    * 根据用户名获取用户详细信息
    */
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

7. UserDetails

public interface UserDetails extends Serializable {
​
    /**
     * 用户拥有的权限列表
     */
    Collection getAuthorities();
​
    /**
     * 密码
     */
    String getPassword();
​
    /**
     * 账号
     */
    String getUsername();
​
    /**
     * 账号是否过期,过期的账号不能进行认证
     */
    boolean isAccountNonExpired();
​
    /**
     * 账号是否被锁定
     */
    boolean isAccountNonLocked();
​
    /**
     * 凭据是否过期,过期的凭据不能进行认证
     */
    boolean isCredentialsNonExpired();
​
    /**
     * 账号是否被启用,未启用的账号不能进行认证
     */
    boolean isEnabled();
}

用户的详细信息,主要用于登录认证。

8. SecurityContextHolder

SecurityContextHolder 是最基本的对象,它负责存储当前 SecurityContext 信息。SecurityContextHolder默认使用 ThreadLocal 来存储认证信息,意味着这是一种与线程绑定的策略。在Web场景下的使用Spring Security,在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。

9. SecurityContext

SecurityContext 负责存储认证通过的用户信息(Authentication对象),保存着当前用户是什么,是否已经通过认证,拥有哪些权限等等。

10. AuthenticationSuccessHandler

AuthenticationSuccessHandler 主要用于认证成功后的处理,比如返回页面或者数据。

11. AuthenticationFailureHandler

AuthenticationFailureHandler 主要用于认证失败后的处理,比如返回页面或者数据。

12. AccessDecisionManager

AccessDecisionManager 主要用于实现权限,决定请求是否具有访问的权限。

13. AccessDeniedHandler

AccessDeniedHandler 主要用于无权访问时的处理

第三节 Spring Security 工作流程

1. DelegatingFilterProxy

public class DelegatingFilterProxy extends GenericFilterBean {}​public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware,        EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean {}

由上面的类定义可以看出,DelegatingFilterProxy是一个 Filter,同时,也是一个InitializingBean。

public interface InitializingBean {
​
    /**
     * 当Bean的所有属性设置后由包含的BeanFactory调用,而BeanFactory就是用来创建bean的,换言之,就是将Bean纳入Spring IOC容器
     */
    void afterPropertiesSet() throws Exception;
}

实现了InitializingBean接口的类,在创建对象并完成属性设置后,会被纳入Spring IOC 容器管理。如果DelegatingFilterProxy在web.xml中配置,那么,在容器启动时就会实例化该Filter,然后完成初始化,随后被纳入 Spring IOC 容器管理。这样就相当于与 Spring 完成整合。

而 DelegatingFilterProxy 由 spring web 提供,与 Spring Security 无关。那么 DelegatingFilterProxy 到底有什么作用呢?

其作用是代理真正的Filter实现类

DelegatingFilterProxy 如何知道其所代理的Filter是哪个呢?

这是通过其自身的targetBeanName的属性来确定的,通过该名称,DelegatingFilterProxy可以从WebApplicationContext中获取指定的 bean 作为代理对象。该属性可以通过在web.xml 中定义 DelegatingFilterProxy 时通过 init-param 来指定,如果未指定,则将取其在web.xml中声明时定义的名称作为 targetBeanName 的值。

        springSecurityFilterChain    org.springframework.web.filter.DelegatingFilterProxy            targetBeanName        springSecurityFilterChain        springSecurityFilterChain    /*

2. FilterChainProxy

使用Spring Security时,DelegatingFilterProxy 代理的就是一个 FilterChainProxy。当我们使用基于Spring Security的NameSpace进行配置时,系统会自动为我们注册一个名为 springSecurityFilterChain 类型为 FilterChainProxy 的 Bean,这也是为什么我们在使用 Spring Security 时需要在 web.xml 中声明一个 name 为 springSecurityFilterChain 的 DelegatingFilterProxy 的 Filter 了。

FilterChainProxy 有什么作用呢?

一个 FilterChainProxy 中可以包含有多个 FilterChain,但是某个请求只会对应一个 FilterChain,而一个 FilterChain 中又可以包含有多个 Filter。

而 Spring Security 底层正是通过一系列的 Filter 来工作的。具体详情如下:

2.1 WebAsyncManagerIntegrationFilter

将Security上下文与SpringWeb中用于处理异步请求映射的WebAsyncmanager进行集成

2.2 SecurityContextPersistenceFilter

在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后麻将SecurityContextHolder中关于这次请求的信息存储的‘仓储’中,然后将SecurityContextHolder中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的

2.3 HeaderWriterFilter

用于将头信息加入响应中

2.4 CsrfFilter

用于处理跨站请求伪造

2.5 LogoutFilter

用于处理退出登录

2.6 UsernamePasswordAuthenticationFilter

[重点]

用于处理基于表单的登录请求,从表单中获取用户名和密码,默认情况下处理来自/login的请求,从表单中获取用户名和密码, 默认使用表单name值为username和password,这两个值可以通过这个过滤器的usernaemparamter个passwordParameter连个参数的值进行修改

2.7 DefaultLoginPageGeneratingFilter

如果没有配置登陆页面,那系统初始化就会配置这个过滤器。并且用于在需要进行登陆时生成一个登陆表单页

2.8 BasicAuthenticationFilter

检测和处理http basic认证

2.9 RequestCacheAwareFilter

用于处理请求的缓存

2.10 SecurityContextHolderAwareRequestFilter

主要包装请求对象request

2.11 AnonymousAuthenticationFilter

检测SecurityContextHolder中是否存在Authentication对象,如果不存在为其提供一个匿名Authentication

2.12 SessionManagementFilter

管理session的过滤器

2.13 ExceptionTranslationFilter

处理AccessDeniedException和AuthenticationException异常

2.14 FilterSecurityInterceptor

可以看作过滤器链的出口

2.15 RememberMeAuthenticationFilter

当用户没有登录而直接访问资源时,从cookie中找出用户的信息,如果SpringSecurity能够识别出用户提供remember me cookie ,用户将不必填写用户名和密码,而是直接登录进入系统,该过滤器默认从不开启

第三节 Spring Security 认证

Spring Security详解 

 

1. 数据库认证

1.1 配置认证管理器

package com.qf.authentication.config;
​
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
​
@EnableWebSecurity //启用security
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    //创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
​
    //认证管理器配置
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //数据库数据认证提供器
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        //设置认证使用的用户详情服务,业就是查询用户信息的服务
        daoAuthenticationProvider.setUserDetailsService();
        //设置密码使用的加密器
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        //设置认证管理构建器使用的认证提供器
        auth.authenticationProvider(daoAuthenticationProvider);
    }
}

1.2 创建查询用户信息服务

package com.qf.authentication.service;
​
import org.springframework.security.core.userdetails.UserDetailsService;
​
//用户业务层,继承了UserDetailsService,方便与security结合
public interface UserService extends UserDetailsService {
}
​
​
package com.qf.authentication.service.impl;
​
import com.qf.authentication.service.UserService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
​
@Service
public class UserServiceImpl implements UserService {
​
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return null;
    }
}

1.4 创建用户实体

package com.qf.authentication.model;
​
import lombok.Data;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
​
import java.util.List;
​
@Data
public class User implements UserDetails {
​
    private String username; //账号
​
    private String password; //密码
​
    private List authorities; //拥有的权限
​
    @Override
    public boolean isAccountNonExpired() { //账号是否未过期
        return true;
    }
​
    @Override
    public boolean isAccountNonLocked() { //账号是否未被锁定
        return true;
    }
​
    @Override
    public boolean isCredentialsNonExpired() {//凭据是否未过期
        return true;
    }
​
    @Override
    public boolean isEnabled() {//账号是否可用
        return true;
    }
}

1.5 完善用户信息服务

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = new User();
    user.setUsername("admin");
    user.setPassword(passwordEncoder.encode("123456"));
    user.setAuthorities(Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"), new SimpleGrantedAuthority("ROLE_USER")));
    return user;
}

1.6 完善认证管理器配置

//认证管理器配置
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //数据库数据认证提供器
    DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
    //设置认证使用的用户详情服务,业就是查询用户信息的服务
    daoAuthenticationProvider.setUserDetailsService(userService);
    //设置密码使用的加密器
    daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
    //设置认证管理构建器使用的认证提供器
    auth.authenticationProvider(daoAuthenticationProvider);
}

1.7 HTTP认证配置

package com.qf.authentication.config;
​
import com.qf.authentication.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
​
@EnableWebSecurity //启用security
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Autowired
    private UserService userService;
​
    //创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
​
    //认证管理器配置
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //数据库数据认证提供器
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        //设置认证使用的用户详情服务,业就是查询用户信息的服务
        daoAuthenticationProvider.setUserDetailsService(userService);
        //设置密码使用的加密器
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        //设置认证管理构建器使用的认证提供器
        auth.authenticationProvider(daoAuthenticationProvider);
    }
​
    //Http认证配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();//关闭跨站请求模拟
        //设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制
        http.formLogin().loginPage("/").loginProcessingUrl("/login")
            .successHandler().failureHandler().permitAll();
        http.authorizeRequests().anyRequest().authenticated();
        //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
        http.logout().invalidateHttpSession(true).permitAll();
    }
}
​

1.8 创建认证处理器

package com.qf.authentication.handler;
​
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
​
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
​
//认证成功的处理器
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
​
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.sendRedirect("/main.html");
    }
}
​
​
package com.qf.authentication.handler;
​
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
​
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
​
//认证失败的处理器
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
​
​
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.sendRedirect("/");
    }
}

1.9 完善HTTP认证配置

//Http认证配置
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();//关闭跨站请求模拟
    //设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制
    http.formLogin().loginPage("/").loginProcessingUrl("/login")
        .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();
    http.authorizeRequests().anyRequest().authenticated();
    //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
    http.logout().invalidateHttpSession(true).permitAll();
}

1.10 页面创建

index.html




    
    Security登录


    
                           

main.html




    
    Security登录成功


    认证通过了

1.11 启动程序进行测试

1.12 核心流程梳理

登录请求被 UsernamePasswordAuthenticationFilter 拦截,该拦截器尝试认证,认证过程中调用 AuthenticationManager进行认证。AuthenticationManager进行认证时,将该认证管理器中的所有认证提供器遍历一遍,遍历过程中,首先检测认证提供器是否支持认证的票据类型,如果支持,则认证提供器开始进行认证。认证提供器认证过程中会调用 UserDetailsService 获取用户信息,然后进行信息比对,如果正确,则返回一个认证通过的票据。所有认证提供器中,只要任意一个认证提供器认证通过,则表示认证成功。

2. 短信认证

2.1 创建短信认证过滤器

package com.qf.authentication.sms;
​
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
​
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
​
//短信认证提供器,模仿UsernamePasswordAuthenticationFilter编写
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
​
    //短信登录使用的URL,请求类型必须时POST
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms","POST");
​
​
    public SmsAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }
​
    public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }
​
    //尝试认证
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
        return null;
    }
​
​
    //获取短信验证码
    @Nullable
    protected String obtainCode(HttpServletRequest request) {
        return request.getParameter("code");
    }
​
    //获取手机号码
    @Nullable
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter("mobile");
    }
}

2.2 创建短信认证票据

package com.qf.authentication.sms;
​
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;
​
import java.util.Collection;
​
//短信认证的票据
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
​
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
​
    //认证之前存储手机号码,认证之后存储的是用户信息,也就是一个User对象
    private final Object principal;
​
    //验证码
    private  Object credentials;
​
    /**
     * 认证之前使用
     * @param principal
     */
    public SmsAuthenticationToken(Object principal, Object credentials){
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(false);
    }
​
    /**
     * 认证之后使用
     * @param principal
     * @param authorities
     */
    public SmsAuthenticationToken(Object principal, Object credentials, Collection authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }
​
    @Override
    public Object getCredentials() {
        return credentials;
    }
​
    @Override
    public Object getPrincipal() {
        return principal;
    }
​
    //这个方法是security框架执行认证流程时调用的,用户不应该调用,应该使用构造方法完成认证
    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated,
                "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }
​
    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

2.3 创建短信实体

package com.qf.authentication.sms;
​
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
​
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SmsCode {
    //手机号
    private String mobile;
    //验证码
    private String code;
    //过期时间
    private long expire;
}

2.4 短信模拟

package com.qf.authentication.controller;
​
import com.qf.authentication.sms.SmsCode;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
​
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
​
​
@Controller
public class SmsController {
​
    @GetMapping("/code")
    public void imgCode(@RequestParam("mobile")String mobile, HttpSession session, HttpServletResponse response) throws IOException {
        BufferedImage bi = new BufferedImage(200, 40, BufferedImage.TYPE_INT_RGB);
        Graphics graphics = bi.getGraphics();
        graphics.setColor(Color.GRAY);
        graphics.fillRect(0, 0, 200, 40);
        StringBuilder builder = new StringBuilder();
        Random r = new Random();
        for(int i=0; i<6; i++){
            int num = r.nextInt(10);
            builder.append(num);
            graphics.setColor(Color.red);
            graphics.drawString(Integer.toString(num), i*10 + 20, 15);
        }
        //创建短信实体
        SmsCode code = new SmsCode(mobile, builder.toString(), System.currentTimeMillis() + 5 * 60 * 1000);
        //将短信实体放入session中
        session.setAttribute("smsCode", code);
        graphics.dispose();
        ImageIO.write(bi, "jpg", response.getOutputStream());
    }
}

2.5 完善短信认证过滤器

//尝试认证
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
    if(!request.getMethod().equalsIgnoreCase("POST")){
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    String code = obtainCode(request);//获取短信验证码
    //从session中获取发送的短信验证码信息
    SmsCode smsCode = (SmsCode) request.getSession().getAttribute("smsCode");
    if(code == null || smsCode == null){
        throw new AuthenticationServiceException("SMS code cannot be null");
    }
    String mobile = obtainMobile(request);//获取手机号
    long currentTime = System.currentTimeMillis();//获取系统当前时间
    if(smsCode.getExpire() < currentTime || !mobile.equals(smsCode.getMobile())){//如果系统当前时间比验证码过期时间还要大,说明验证码过期,手机号码与验证码不匹配
        throw new AuthenticationServiceException("SMS code is invalid:" + smsCode.getCode());
    } else if(!code.equals(smsCode.getCode())){
        throw new AuthenticationServiceException("SMS code error");
    }
    SmsAuthenticationToken token = new SmsAuthenticationToken(mobile, code);//创建SMS token
    this.setDetails(request, token);
    return this.getAuthenticationManager().authenticate(token);//调用认证管理器认证token
}
​
//将请求信息放入token中
protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) {
    authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}

2.6 创建短信认证提供器

package com.qf.authentication.sms;
​
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
​
public class SmsAuthenticationProvider implements AuthenticationProvider {
​
    //获取认证用户的信息的服务接口
    private UserDetailsService userDetailsService;
​
    /**
     * 这个方法就是认证,如果没有抛出认证异常,说明认证成功
     * @param authentication  未进行认证的信息,里面就是包含了一个mobile信息和请求的信息
     * @return 返回一个认证完成的信息
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String mobile = (String) authentication.getPrincipal();
        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile); //通过手机号码获取用户信息
        SmsAuthenticationToken authenticationToken = new SmsAuthenticationToken(userDetails, authentication.getCredentials(), userDetails.getAuthorities());
        authenticationToken.setDetails(authentication.getDetails());
        return authenticationToken;
    }
​
    @Override
    public boolean supports(Class authentication) {
        // 判断 authentication 是不是 SmsCodeAuthenticationToken 类型或者其子类或者其子接口
        return SmsAuthenticationToken.class.isAssignableFrom(authentication);
    }
​
    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }
​
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

2.7 配置短信认证

package com.qf.authentication.config;
​
import com.qf.authentication.handler.LoginFailureHandler;
import com.qf.authentication.handler.LoginSuccessHandler;
import com.qf.authentication.service.UserService;
import com.qf.authentication.sms.SmsAuthenticationFilter;
import com.qf.authentication.sms.SmsAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
​
@EnableWebSecurity //启用security
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Autowired
    private UserService userService;
​
    @Autowired
    private LoginSuccessHandler loginSuccessHandler;
​
    @Autowired
    private LoginFailureHandler loginFailureHandler;
​
    @Autowired
    @Qualifier("authenticationManagerBean") //表示使用指定名称的认证管理器
    private AuthenticationManager authenticationManager;
​
    //创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
​
    //认证管理器配置
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //创建短信认证提供器
        SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
        smsAuthenticationProvider.setUserDetailsService(userService);
        //将认证提供器添加到认证管理器中
        auth.authenticationProvider(smsAuthenticationProvider);
​
        //数据库数据认证提供器
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        //设置认证使用的用户详情服务,业就是查询用户信息的服务
        daoAuthenticationProvider.setUserDetailsService(userService);
        //设置密码使用的加密器
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        //设置认证管理构建器使用的认证提供器
        auth.authenticationProvider(daoAuthenticationProvider);
    }
​
    //Http认证配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();//关闭跨站请求模拟
        //设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制
        http.formLogin().loginPage("/").loginProcessingUrl("/login")
                .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();
        //设置获取验证码的请求放行
        http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll()
                //其他请求需要认证
                .anyRequest().authenticated();
        //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
        http.logout().invalidateHttpSession(true).permitAll();
        //将短信认证过滤器添加账号密码过滤器的前面
        http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
​
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
​
    @Bean
    public SmsAuthenticationFilter smsAuthenticationFilter() throws Exception {
        SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
        //设置短信认证过滤器使用的认证管理器
        smsAuthenticationFilter.setAuthenticationManager(authenticationManager);
        //设置登录成功的处理器
        smsAuthenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
        //设置登录失败的处理器
        smsAuthenticationFilter.setAuthenticationFailureHandler(loginFailureHandler);
        return smsAuthenticationFilter;
    }
}

2.8 修改登录页面

index.html




    
    Security登录


    
                           
​    
                                   

2.9 启动程序进行测试

第四节 Spring Security 授权

Spring Security详解 

 

1. 启用注解授权

@EnableWebSecurity //启用security//prePostEnabled = true启用@PreAuthorize()//securedEnabled = true启用@Secured()//jsr250Enabled = true启用@RolesAllowed、@PermitAll、@DenyAll 但该注解需要jar包支撑@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter {}

JSR 250 依赖包



    javax.annotation
    jsr250-api
    1.0

2. HTTP授权配置

//Http认证配置@Overrideprotected void configure(HttpSecurity http) throws Exception {    http.csrf().disable();//关闭跨站请求模拟    //设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制    http.formLogin().loginPage("/").loginProcessingUrl("/login")        .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();    //设置获取验证码的请求放行    http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll()         //授权请求表示任意请求都需要认证才能够访问        .anyRequest().authenticated();    //设置异常处理使用访问拒绝处理器    http.exceptionHandling().accessDeniedHandler();    //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制    http.logout().invalidateHttpSession(true).permitAll();    //将短信认证过滤器添加账号密码过滤器的前面    http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}

3. 创建拒绝请求处理器

package com.qf.authentication.handler;​import org.springframework.security.access.AccessDeniedException;import org.springframework.security.web.access.AccessDeniedHandler;import org.springframework.stereotype.Component;​import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;​//HTTP请求被拒绝的处理器@Componentpublic class RequestDeniedHandler implements AccessDeniedHandler {​    //这里就是拒绝处理的具体步骤实现    @Override    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {        response.setContentType("text/html;charset=utf-8");        //返回拒绝处理的信息        response.getWriter().print(accessDeniedException.getMessage());    }}

5. 完善HTTP授权配置

@Autowiredprivate RequestDecisionManager decisionManager;​@Autowiredprivate RequestDeniedHandler deniedHandler;​//Http认证配置@Overrideprotected void configure(HttpSecurity http) throws Exception {    http.csrf().disable();//关闭跨站请求模拟    //设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制    http.formLogin().loginPage("/").loginProcessingUrl("/login")        .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();    //设置获取验证码的请求放行    http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll()         //授权请求表示任意请求都需要认证才能够访问        .anyRequest().authenticated();    //设置异常处理使用访问拒绝处理器    http.exceptionHandling().accessDeniedHandler(deniedHandler);    //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制    http.logout().invalidateHttpSession(true).permitAll();    //将短信认证过滤器添加账号密码过滤器的前面    http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}

6. 创建测试请求

package com.qf.authentication.controller;​import org.springframework.security.access.annotation.Secured;import org.springframework.security.access.prepost.PreAuthorize;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;​import javax.annotation.security.RolesAllowed;​@RestControllerpublic class TestController {​    @GetMapping("/test1")    @PreAuthorize("hasRole('ROLE_ADMIN')")    public String test1(){        return "test1";    }​    @GetMapping("/test2")    @PreAuthorize("hasRole('ROLE_TEST')")    public String test2(){        return "test2";    }​    @GetMapping("/test3")    @Secured("ROLE_ADMIN")    public String test3(){        return "test3";    }​    @GetMapping("/test4")    @Secured("ROLE_TEST")    public String test4(){        return "test4";    }​    @GetMapping("/test5")    @RolesAllowed("ROLE_ADMIN")    public String test5(){        return "test5";    }​    @GetMapping("/test6")    @RolesAllowed("ROLE_TEST")    public String test6(){        return "test6";    }}

7. 启动程序进行测试

第五节 Security 与 AJAX 对接

这个问题的根本原因在于登录结果的处理和拒绝访问的处理。如果能够判断一个请求是ajax请求,那么问题即将得到解决。

package com.qf.security.util;
​
import javax.servlet.http.HttpServletRequest;
​
/**
 * @Author: wu
 * @Description:
 * @Date: 2021-11-09
 */
//针对请求相关的操作工具类
public class RequestUtil {
​
    private RequestUtil(){}
​
    /**
     * 验证请求是否是AJAX请求 这种验证对于jQuery发送的AJAX没有任何问题
     * 但是,对于 axios发送的AJAX可能存在没有X-Requested-With这个头信
     * 息的
     * @param request
     * @return
     */
    public static boolean isAjaxRequest(HttpServletRequest request){
        String header = request.getHeader("X-Requested-With");
        return "XMLHttpRequest".equalsIgnoreCase(header);
    }
}

登录的时候传递的参数在过滤器中获取不到,需要注意:在传递参数的时候要使用get方法传递参数的方式对参数进行拼接,然后赋值给data

$.ajax({
    type: 'post',
    url: 'login',
    data: "username="+ $("#username").val() + "&password=" + $("#password").val(),
    success: function (resp) {
        console.log(resp);
    }
});

本文来自网络,不代表协通编程立场,如若转载,请注明出处:https://net2asp.com/6be342483b.html