Spring Security

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张图片

1. Authentication 认证

//这个接口的实现类都是Token,称为票据,UsernamePasswordAuthenticationToken
//我们自定义实现就可以叫MobileCodeAuthenticationToken
public interface Authentication extends Principal, Serializable {

	/**
	 * 权限列表
	 */
	Collection<? extends GrantedAuthority> 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<AuthenticationProvider> 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<? extends GrantedAuthority> 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 容器管理。如果DelegatingFilterProxyweb.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 的值


<filter>
    
    <filter-name>springSecurityFilterChainfilter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
    <init-param>
    	<param-name>targetBeanNameparam-name>
        <param-value>springSecurityFilterChainparam-value>
    init-param>
filter>
<filter-mapping>
    <filter-name>springSecurityFilterChainfilter-name>
    <url-pattern>/*url-pattern>
filter-mapping>

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_第2张图片

1. 数据库认证

1.1 配置认证管理器
package com.ch.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.ch.authentication.service;

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

//用户业务层,继承了UserDetailsService,方便与security结合
public interface UserService extends UserDetailsService {
}


package com.ch.authentication.service.impl;

import com.ch.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.ch.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<SimpleGrantedAuthority> 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.ch.authentication.config;

import com.ch.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.ch.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.ch.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

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Security登录title>
head>
<body>
    <form action="login" method="post">
        <input type="text" name="username">
        <input type="password" name="password">
        <input type="submit" value="登录">
    form>
body>
html>

main.html

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Security登录成功title>
head>
<body>
    认证通过了
body>
html>
1.11 启动程序进行测试
1.12 核心流程梳理

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

2. 短信认证

2.1 创建短信认证过滤器
package com.ch.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.ch.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<? extends GrantedAuthority> 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.ch.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.ch.authentication.controller;

import com.ch.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.ch.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.ch.authentication.config;

import com.ch.authentication.handler.LoginFailureHandler;
import com.ch.authentication.handler.LoginSuccessHandler;
import com.ch.authentication.service.UserService;
import com.ch.authentication.sms.SmsAuthenticationFilter;
import com.ch.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

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Security登录title>
head>
<body>
    <form action="login" method="post">
        <input type="text" name="username">
        <input type="password" name="password">
        <input type="submit" value="登录">
    form>

    <form action="sms" method="post" >
        <input type="text" name="mobile">
        <input type="text" name="code">
        <input type="button" value="获取验证码" onclick="getCode()">
        <input type="submit" value="登录">
    form>
body>
<script type="text/javascript">
    function getCode(){
        let elements = document.getElementsByName("code");
        let img = document.createElement("img");
        img.src = "code?mobile=" + document.getElementsByName("mobile")[0].value;
        elements[0].after(img);
    }
script>
html>
2.9 启动程序进行测试

第四节 Spring Security 授权

Spring Security_第3张图片

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 依赖包


<dependency>
    <groupId>javax.annotationgroupId>
    <artifactId>jsr250-apiartifactId>
    <version>1.0version>
dependency>

2. 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().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.ch.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请求被拒绝的处理器
@Component
public 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授权配置

@Autowired
private RequestDecisionManager decisionManager;

@Autowired
private RequestDeniedHandler deniedHandler;

//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();
    //设置异常处理使用访问拒绝处理器
    http.exceptionHandling().accessDeniedHandler(deniedHandler);
    //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制
    http.logout().invalidateHttpSession(true).permitAll();
    //将短信认证过滤器添加账号密码过滤器的前面
    http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}

6. 创建测试请求

package com.ch.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;

@RestController
public 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.ch.security.util;

import javax.servlet.http.HttpServletRequest;

//针对请求相关的操作工具类
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);
    }
});

你可能感兴趣的:(spring,java,后端)