文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Spring Security详解

2023-09-01 08:18

关注

第一节 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 核心组件

 

1. Authentication 认证

//这个接口的实现类都是Token,称为票据,UsernamePasswordAuthenticationToken//我们自定义实现就可以叫MobileCodeAuthenticationTokenpublic interface Authentication extends Principal, Serializable {​        Collection extends GrantedAuthority> getAuthorities();​        Object getCredentials();​        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 {​        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>    public SmsAuthenticationToken(Object principal, Object credentials){        super(null);        this.principal = principal;        this.credentials = credentials;        super.setAuthenticated(false);    }​        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.qf.authentication.sms;​import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;​@Data@AllArgsConstructor@NoArgsConstructorpublic 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;​​@Controllerpublic 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 完善短信认证过滤器

//尝试认证@Overridepublic 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;​        @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 //启用securitypublic 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

<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 授权

 

 

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认证配置@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;​//针对请求相关的操作工具类public class RequestUtil {​    private RequestUtil(){}​        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://blog.csdn.net/JAVA_KX/article/details/130691948

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯