前端带你学后端系列 ⑤【安全框架Spring Security篇一】

前端带你学后端系列 ⑤【安全框架Spring Security篇一】

  • Ⅰ 什么是Spring Security?
    • ① Spring Security概念
    • ② Spring Security 本质
    • ③ Spring Security核心组件
      • ① 用图片详细展示
      • ② 用代码详细展示 AuthenticationManager 与 AuthenticationProvider 与 UserDetailsService的关系
    • ④ 认证流程
  • Ⅱ Spring Security应用
    • ① HTTP基本认证
      • 前后端分离模式下,验证的写法
      • ① 自定义过滤器filter使用Json数据格式认证登录
      • ② 在SpringSecurity配置类中配置替换UsernamePasswordAuthenticationFilter
    • ② Form认证
    • ③ 前后端分离时的安全处理方案(最常用)
    • ④ 基于过滤器实现图形验证码
      • ① 添加依赖
      • ② 验证码配置类
      • ③ 创建控制层,给前端返回验证码的图片
      • ④ 自定义过滤器链,且放在【UsernamePasswordAuthenticationFilter】过滤器的前面
      • ⑤ 配置Security配置类
    • ⑤ Remember-Me 记住我功能(传统web)
      • ① yml文件配置加密令牌的key
      • ② 配置类中配置
    • ⑤ Remember-Me 记住我功能(前后端分离)
      • ① 自定义认证类 LoginFilter
      • ② 自定义 RememberMeService
      • ③ 配置

Ⅰ 什么是Spring Security?

① Spring Security概念

Spring Security 是基于 Spring 的【身份认证】和【用户授权】,其中核心技术使用了 【Servlet 过滤器】、【IOC】 和【AOP】等。

【身份认证】:用户去访问系统资源时,系统要求验证用户的身份信息。拿着登录网站的用户名密码和数据库的比对
【用户授权】:当身份认证通过后,去访问系统的资源,给用户分配角色进而权限

② Spring Security 本质

Spring Security的本质是过滤器链,Security会依次按照顺序执行过滤器链

前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第1张图片


前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第2张图片


接下来,我们认识一下这些过滤器链

前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第3张图片

各个过滤器链的作用

前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第4张图片


前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第5张图片

  1. SecurityContextPersistenceFilter:是【过滤链】的开始,它会确认当前【用户的认证信息】,放入【 SecurityContextHolder 】当中。
  2. HeaderWriterFilter:往请求的 Header 中添加一些信息。
  3. CorsFilter:用于处理【跨域请求】
  4. LogoutFilter:匹配【退出请求】默认为【 /logout】,清理认证信息。
  5. UsernamePasswordAuthenticationFilter:匹配【登录请求】默认为 【/login 的 POST 请求】,用来封装表单提交的【用户名密码】,封装成 【UsernamePasswordAuthenticationToken】,更新数据到【 SecurityContextHolder 】中。
  6. ExceptionTranslationFilter:异常过滤,处于整个链【后部】,用来转化访问异常 AccessDeniedException 和认证异常 AuthenticationException
  7. FilterSecurityInterceptor:用于判断和处理请求访问需要的权限,角色等信息。

③ Spring Security核心组件

  1. SecurityContextHolder:容器。用于【存储】已经登陆用户的【信息】。它通常包含了当前登录用户的信息,例如用户名、密码和角色等。
  2. SecurityContext:持有Authentication被SecurityContextHolder 所持有
  3. Authentication:在用户登录认证之前用户名密码等信息会被封装为一个Authentication的具体实现类对象。在登录认证成功之后 又会生成一个信息更全面的Authentication对象。放到SecurityContextHolder中,供全局使用
  4. AuthenticationManager:它【处理】来自用户的身份验证请求,并基于这些请求【返回认证对象】
  5. AuthenticationProvider:【具体】的身份验证机制,例如表单身份验证。生成一个【 Authentication 对象】 【返回】给【 AuthenticationManager】
  6. UserDetailsService:定义了从【内存、关系数据库】中【获取】用户详细信息的方法。UserDetailsService 只有 loadUserByUsername 一个接口方法, 用于通过用户名获取用户数据. 返回 UserDetails 对象, 表示用户的核心信息 (用户名, 用户密码, 权限等信息)。

① 用图片详细展示

  1. SecurityContextHolder与SecurityContext与Authentication的关系。
    前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第6张图片
  2. AuthenticationManager 与 AuthenticationProvider 与 UserDetailsService的关系

前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第7张图片

② 用代码详细展示 AuthenticationManager 与 AuthenticationProvider 与 UserDetailsService的关系



UserDetailsService

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private AuthenticationMapper authenticationMapper;
    
    /*
       UserDetailsService 只有 loadUserByUsername 一个接口方法。
       用于查询数据库中的数据,将来做比对用。  
    */
    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
    
        //1. 根据用户名查询数据库,查到对应的用户
        MyUser myUser = authenticationMapper.loadUserByUsername(name);

        // ... 做一些异常处理,没有找到用户之类的
        if (myUser == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        //2. 根据用户ID,查询用户的角色
        List<Role> roles = authenticationMapper.findRoleByUserId(myUser.getId());
        // 添加角色
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        
        //3. 构建 Security 的 User 对象,返回给AuthenticationProvider 
        return new User(myUser.getName(), myUser.getPassword(), authorities);
    }

    @Autowired
    public void setAuthenticationMapper(AuthenticationMapper authenticationMapper) {
        this.authenticationMapper = authenticationMapper;
    }
}

AuthenticationProvider

@Component
public class MyAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsServiceImpl userDetailsServiceImpl;

    private BCryptPasswordEncoder bCryptPasswordEncoder;

    /*
       Authentication authenticate():定义了用于处理认证逻辑的接口标准.
       
    */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
        //1. 获取用户输入的用户名和密码
        final String username = authentication.getName();
        final String password = authentication.getCredentials().toString();
        
        //2. 获取userDetailsServiceImpl返回的对象
        UserDetails userDetails = userDetailsServiceImpl.loadUserByUsername(username);
        
        //3. 进行密码的比对
        boolean flag = bCryptPasswordEncoder.matches(password, userDetails.getPassword());
        // 校验通过
        if (flag) {
            //4. 将权限信息也封装进去,返回给AuthenticationManager
            return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
        }

        	throw new AuthenticationException("用户密码错误") {
        };
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }

  

    @Autowired
    public void setBCryptPasswordEncoder(BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Autowired
    public void setUserDetailsServiceImpl(UserDetailsServiceImpl userDetailsServiceImpl) {
        this.userDetailsServiceImpl = userDetailsServiceImpl;
    }
}

SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private MyAuthenticationProvider myAuthenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        // 配置自定义的校验器
        auth.authenticationProvider(myAuthenticationProvider);
    }

    // ~ Bean
    // -----------------------------------------------------------------------------------------------------------------

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // ~ Autowired
    // -----------------------------------------------------------------------------------------------------------------

    @Autowired
    public void setMyAuthenticationProvider(MyAuthenticationProvider myAuthenticationProvider) {
        this.myAuthenticationProvider = myAuthenticationProvider;
    }
}

④ 认证流程

前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第8张图片

Ⅱ Spring Security应用

① HTTP基本认证

前后端分离项目一般用这个

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //1.配置基本认证方式
        http.authorizeRequests()
            	//对任意请求都进行认证
                .anyRequest()
                .authenticated()
                .and()
                //开启basic认证。默认就是这个
                .httpBasic();
    }

}

前后端分离模式下,验证的写法

① 自定义过滤器filter使用Json数据格式认证登录

①定义一个LoginFilter 继承自UsernamePasswordAuthenticationFilter,当进行认证时,请求不再进入UsernamePasswordAuthenticationFilter,而是进入LoginFilter

public class LoginFilter extends UsernamePasswordAuthenticationFilter {
   
     

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
   
     
        // 1. 判断是否是post请求方式
        if(!request.getMethod().equals("POST")){
   
     
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        // 2. 判断是否是json格式请求类型
        if(request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)){
   
     
            // 3. 从json数据中获取用户输入用户名和密码认证:{"uname":"xxx","password":"xxx"}
            try {
   
     
                Map<String,String> userInfo = new HashMap<>();
                userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = userInfo.get(getUsernameParameter());
                String password = userInfo.get(getPasswordParameter());
                System.out.println("username = " + username);
                System.out.println("password = " + password);
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);
                setDetails(request,authenticationToken);
                return this.getAuthenticationManager().authenticate(authenticationToken);
            } catch (IOException e) {
   
     
                throw new RuntimeException(e);
            }
        }
        // 4. 如果不是json格式,那么按照父类的表单登录逻辑处理
        return super.attemptAuthentication(request,response);
    }
}

② 在SpringSecurity配置类中配置替换UsernamePasswordAuthenticationFilter

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Bean
    public UserDetailsService userDetailsService(){
   
     
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
     
        auth.userDetailsService(userDetailsService());
    }

    // 暴露自定义的authenticationManager
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
   
     
        return super.authenticationManager();
    }

    @Bean
    public LoginFilter loginFilter() throws Exception {
   
     
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setFilterProcessesUrl("/doLogin");
        loginFilter.setUsernameParameter("uname");
        loginFilter.setPasswordParameter("passwd");
        loginFilter.setAuthenticationManager(authenticationManager());
        loginFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
        loginFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
        return loginFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable();
        // 将loginFilter过滤器添加到UsernamePasswordAuthenticationFilter过滤器所在的位置
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

② Form认证

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 用来定义哪些请求需要忽略安全控制,哪些请求必须接受安全控制;还可以在合适的时候清除SecurityContext以避免内存泄漏,
     * 同时也可以用来定义请求防火墙和请求拒绝处理器,另外我们开启Spring Security Debug模式也是这里配置的
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        //super.configure(web);
        web.ignoring()
                .antMatchers("/js/**", "/css/**", "/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);

        //3.进一步配置自定义的登录页面   
       //拦截请求,创建FilterSecurityInterceptor 
       http.authorizeRequests() 
                .anyRequest()
                .authenticated()
                //用and来表示配置过滤器结束,以便进行下一个过滤器的创建和配置
                .and() 
                //设置表单登录,创建UsernamePasswordAuthenticationFilter
                .formLogin()                    
                .loginPage("/myLogin.html")
                .permitAll()
                //指登录成功后,是否始终跳转到登录成功url。它默认为false
                .defaultSuccessUrl("/index.html",true)
                //post登录接口,登录验证由系统实现
                .loginProcessingUrl("/login")
                //用户密码错误跳转接口
                .failureUrl("/error.html")
                //要认证的用户参数名,默认username
                .usernameParameter("username")
                //要认证的密码参数名,默认password
                .passwordParameter("password")
                .and()
                //配置注销
                .logout()
                //注销接口
                .logoutUrl("/logout")
                //注销成功后跳转到的接口
                .logoutSuccessUrl("/myLogin.html")
                .permitAll()
                //删除自定义的cookie
                .deleteCookies("myCookie")
                .and()
                //注意:需禁用crsf防护功能,否则登录不成功
                .csrf()
                .disable();
    }

}

指定自定义登陆页面与error页面

<body>
    <div class="login">
        <h2>Access Formh2>
        <div class="login-top">
            <h1>登录验证h1>
            <form action="/login" method="post">
                <input type="text" name="username" placeholder="username" />
                <input type="password" name="password" placeholder="password" />
                <div class="forgot">
                    <a href="#">忘记密码a>
                    <input type="submit" value="登录" >
                div>
            form>
        div>
        <div class="login-bottom">
            <h3>新用户 <a href="#">注 册a>h3>
        div>
    div>
body>

③ 前后端分离时的安全处理方案(最常用)

前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第9张图片

简单版的代码实现

/**
 * 处理登录成功时的业务逻辑
 */
public class SecurityAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    /**
     * Authentication:携带登录的用户名及角色等信息
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        //直接输出json格式的响应信息
        Object principal = authentication.getPrincipal();
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        //以json格式对外输出身份信息
        out.write(new ObjectMapper().writeValueAsString(principal));
        out.flush();
        out.close();
    }
}
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
                http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .permitAll()
                //认证成功时的处理器
                .successHandler(new SecurityAuthenticationSuccessHandler())
                .and()
                .csrf()
                .disable();
    }

}

同样,配置异常的也是如此

/**
 * 处理登录失败时的业务逻辑
 */
public class SecurityAuthenticationFailureHandler extends ExceptionMappingAuthenticationFailureHandler {

    /**
     * AuthenticationException:异常信息
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        //直接输出json格式的响应信息
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write(e.getMessage());
        out.flush();
        out.close();
    }
}
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
                http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .permitAll()
                //认证成功时的处理器
                .successHandler(new SecurityAuthenticationSuccessHandler())
                //认证失败时的处理器
                .failureHandler(new SecurityAuthenticationFailureHandler())
                .and()
                .csrf()
                .disable();
    }

}

④ 基于过滤器实现图形验证码

前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第10张图片
我们推荐使用github上的开源验证码解决方案kaptcha
具体的代码步骤

① 添加依赖

<!--验证码-->
        <dependency>
            <groupId>com.github.axet</groupId>
            <artifactId>kaptcha</artifactId>
            <version>0.0.9</version>
        </dependency>

② 验证码配置类

/**
 * 验证码配置,用于配置验证码的边框,图片,字体等大小。
 */
 
@Configuration
public class KaptchaConfig {

     //验证码的配置类
    @Bean
    DefaultKaptcha producer() {
        Properties properties = new Properties();                       
        properties.put("kaptcha.border", "no");                       //边框
        properties.put("kaptcha.textproducer.font.color", "black");   //字体颜色
        properties.put("kaptcha.textproducer.char.space", "5");       //字体间隔
        properties.put("kaptcha.image.height", "40");                 //图片高度
        properties.put("kaptcha.image.width", "100");                 //图片宽度
        properties.put("kaptcha.textproducer.font.size", "30");       //字体大小
        Config config = new Config(properties);                       
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();         
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

③ 创建控制层,给前端返回验证码的图片

/**
 *  创建验证码图片,返回前端。
 */
 
@Controller
public class CaptchaController {

    @Autowired
    private Producer captchaProducer;

    @GetMapping("/captcha")
    public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 设置内容类型
        response.setContentType("image/jpeg");
        // 创建验证码文本
        String capText = captchaProducer.createText();
        
        // 将验证码文本设置到session
        request.getSession().setAttribute("captcha", capText);
        
        // 创建验证码图片
        BufferedImage bi = captchaProducer.createImage(capText);
        // 获取响应输出流
        ServletOutputStream out = response.getOutputStream();
        // 将图片验证码数据写到响应输出流
        ImageIO.write(bi, "jpg", out);
        
        // 推送并关闭响应输出流
        try {
            out.flush();
        } finally {
            out.close();
        }
    }

}

④ 自定义过滤器链,且放在【UsernamePasswordAuthenticationFilter】过滤器的前面

/**
 * 基于过滤器实现图形验证码的验证功能,这属于Servlet层面,简单易理解.
 * 主要的作用:
 * 	 1. 【重点】放在【UsernamePasswordAuthenticationFilter】前面。
 * 	 2. 如果走的不是“/login”接口,那么不走验证码的验证。
 * 	 3. 如果走的是“/login”,则走验证码的功能。验证成功以后,放行。
 */
public class VerificationCodeFilter extends OncePerRequestFilter {

    private AuthenticationFailureHandler authenticationFailureHandler = new SecurityAuthenticationFailureHandler();

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 非登录请求不校验验证码,直接放行
        if (!"/login".equals(httpServletRequest.getRequestURI())) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } else {
            try {
                //校验验证码
                verificationCode(httpServletRequest);
                
                //验证码校验通过后,对请求进行放行
                filterChain.doFilter(httpServletRequest, httpServletResponse);
            } catch (VerificationCodeException e) {
                authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
            }
        }
    }

    public void verificationCode (HttpServletRequest httpServletRequest) throws VerificationCodeException {
        HttpSession session = httpServletRequest.getSession();
        String savedCode = (String) session.getAttribute("captcha");
        if (!StringUtils.isEmpty(savedCode)) {
            // 随手清除验证码,不管是失败还是成功,所以客户端应在登录失败时刷新验证码
            session.removeAttribute("captcha");
        }

        String requestCode = httpServletRequest.getParameter("captcha");
        // 校验不通过抛出异常
        if (StringUtils.isEmpty(requestCode) || StringUtils.isEmpty(savedCode) || !requestCode.equals(savedCode)) {
            throw new VerificationCodeException();
        }
    }

}

⑤ 配置Security配置类

@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/app/api/**", "/captcha")    // antMatchers 放行验证码
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf()
                .disable();

        //将过滤器添加在UsernamePasswordAuthenticationFilter之前
        http.addFilterBefore(new VerificationCodeFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

}

总结一下步骤

  1. 放行验证码的api请求,前端登陆页面展示验证码图片

  2. 自定义验证码Filter,且放在UsernamePasswordAuthenticationFilter之前
    2.1.1 如果走的是login方法,则走验证码验证的方法
    2.1.2 如果不走login方法,则放行。

⑤ Remember-Me 记住我功能(传统web)

前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第11张图片

前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第12张图片

具体的步骤

① yml文件配置加密令牌的key

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db-security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT
    username: root
    password: syc
  security:
    remember-me:
      key: mySecret

② 配置类中配置

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login/page")
                .loginProcessingUrl("/login")
                .and()
                .authorizeRequests()
                .antMatchers("/login/page").permitAll()
                .anyRequest().authenticated()
                .and()
                .rememberMe() //记住我功能
                .tokenRepository(jdbcTokenRepository())  //保存登录信息
                .tokenValiditySeconds(60 * 60 * 24 * 7); //记住我有效时长;
        http.csrf().disable();
    }

前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第13张图片

⑤ Remember-Me 记住我功能(前后端分离)

前端带你学后端系列 ⑤【安全框架Spring Security篇一】_第14张图片

具体的步骤

① 自定义认证类 LoginFilter


public class LoginFilter extends UsernamePasswordAuthenticationFilter {
 
    private  String passwordParameter  =SPRING_SECURITY_FORM_PASSWORD_KEY;
    private  String usernameParameter  =SPRING_SECURITY_FORM_USERNAME_KEY;
    private String rememberMeParameter = SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY ;
 
 
    /**
     * 重写获取用户信息的方法
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
 
        try {
            //2.判断是否是 json 格式请求类型
            if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
                // 获取JSON 流数据
                ServletInputStream inputStream = request.getInputStream();
                Map<String,String> map = new ObjectMapper().readValue(inputStream, Map.class);
 
                // 获取用户名
 
                String userName = map.get( usernameParameter);
                String pwd = map.get(passwordParameter);
                String rememberMe = map.get(rememberMeParameter);
 
                if (StringUtils.isEmpty(userName)){
                    userName = "";
                }
                if (StringUtils.isEmpty(pwd)){
                    pwd = "";
                }
 
                if (!ObjectUtils.isEmpty(rememberMe)){
                    request.setAttribute(rememberMeParameter,rememberMe);
                }
 
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                        userName, pwd);
 
                // Allow subclasses to set the "details" property
                setDetails(request, authRequest);
 
                return this.getAuthenticationManager().authenticate(authRequest);
            }else {
                throw new AuthenticationServiceException("认证参数格式不正确!");
            }
 
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
 
    @Override
    public void setPasswordParameter(String passwordParameter) {
        this.passwordParameter = passwordParameter;
    }
 
    @Override
    public void setUsernameParameter(String usernameParameter) {
        this.usernameParameter = usernameParameter;
    }
 
    public void setRememberMeParameter(String rememberMeParameter) {
        this.rememberMeParameter = rememberMeParameter;
    }
}

② 自定义 RememberMeService


public class MyPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {
 
    private boolean alwaysRemember;
    public MyPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService, tokenRepository);
    }
 
    @Override
    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        if (alwaysRemember) {
            return true;
        }
        // 这里修改为从 请求作用域中获取
        String paramValue = request.getAttribute(parameter).toString();
        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;
    }
 
    public boolean isAlwaysRemember() {
        return alwaysRemember;
    }
 
    @Override
    public void setAlwaysRemember(boolean alwaysRemember) {
        this.alwaysRemember = alwaysRemember;
    }
}

③ 配置


@Configuration(proxyBeanMethods = true) // 这里如果是 false 则每次使用bean 时都会新建
public class WebSecurityConfig  extends WebSecurityConfigurerAdapter {
 
    /**
     * 自定义数据源
     */
    @Override
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("{noop}123").roles("admin").build());
        return inMemoryUserDetailsManager;
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }
 
    /**
     * 暴露本地AuthenticationManagerBuilder
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
 
    /**
     * 导入自定义Filter
     */
    @Bean
    public LoginFilter loginFilter () throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setUsernameParameter("userName");
        loginFilter.setPasswordParameter("pwd");
        loginFilter.setFilterProcessesUrl("/doLogin");
 
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        loginFilter.setRememberMeServices(rememberMeServices()); // 设置 认证成功时使用自定义rememberMeService 认证成功后 生成cookie 的
        loginFilter.setAuthenticationSuccessHandler(
                (request,response,authentication)->{
                    Map<String,Object> result = new HashMap<>(3);
                    result.put("msg","登录成功!");
                    result.put("code",200);
                    result.put("authen",authentication);
 
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(result);
                    response.setStatus(HttpStatus.OK.value());
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.getWriter().println(json);
 
                }
        );
        loginFilter.setAuthenticationFailureHandler((request,response,authentication)->{
            Map<String,Object> result = new HashMap<>(3);
            result.put("msg","登录失败!");
            result.put("code",500);
            result.put("authen",authentication);
 
            ObjectMapper objectMapper = new ObjectMapper();
            String json = objectMapper.writeValueAsString(result);
            response.setStatus(HttpStatus.NO_CONTENT.value());
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            response.getWriter().println(json);
 
        });
        return loginFilter ;
    }
 
    /**
     * 自定义安全
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/doLogin").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint((request,response,exception)->{
                    Map<String,Object> result = new HashMap<>(3);
                    result.put("msg","未登录!");
                    result.put("code",401);
                    result.put("error",exception.getMessage());
 
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(result);
                    response.setStatus(HttpStatus.EXPECTATION_FAILED.value());
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.getWriter().println(json);
                })
                .and()
                .formLogin()
                .usernameParameter("userName")
                .passwordParameter("pwd")
                .loginProcessingUrl("/doLogin")
                .successHandler((request,response,authentication)->{
                    Map<String,Object> result = new HashMap<>(3);
                    result.put("msg","登录成功!");
                    result.put("code",200);
                    result.put("authen",authentication);
 
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(result);
                    response.setStatus(HttpStatus.OK.value());
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.getWriter().println(json);
 
                })
                .failureHandler((request,response,authentication)->{
                    Map<String,Object> result = new HashMap<>(3);
                    result.put("msg","登录失败!");
                    result.put("code",500);
                    result.put("authen",authentication);
 
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(result);
                    response.setStatus(HttpStatus.NO_CONTENT.value());
                    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    response.getWriter().println(json);
 
                })
                .and()
                .rememberMe()
                .rememberMeServices(rememberMeServices()) // 设置自动登录时使用rememberService
                .and()
                .logout()
                .logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/logout", HttpMethod.GET.name()),
                        new AntPathRequestMatcher("/logout",HttpMethod.POST.name())
                ))
                .logoutSuccessHandler((res,resp,authentication)->{
                    Map<String,Object> rs = new HashMap<>();
                    rs.put("msg","退出登录成功");
                    rs.put("用户信息",authentication);
                    resp.setStatus(HttpStatus.OK.value());
                    String json = new ObjectMapper().writeValueAsString(rs);
 
                    resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
 
                    resp.getWriter().println(json);
                })
                .and()
                .csrf()
                .disable();
 
        // 替换掉这个Filter
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }
 
    /**
     * 引入自定义RememberMeService
     */
    @Bean
    public RememberMeServices rememberMeServices(){
        // 这个 如果是用内存的话不能new 需要让他是对象 才可以,不然数据会不一致
        MyPersistentTokenBasedRememberMeServices myPersistentTokenBasedRememberMeServices = new MyPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),userDetailsService(),new InMemoryTokenRepositoryImpl());
        return myPersistentTokenBasedRememberMeServices;
    }
}
//        将自定义的Filter放在指定Filter后
        http.addFilterAfter();
//        将自定义的Filter放在指定Filter后
        http.addFilterBefore();
//        将自定义的Filter替换指定的原生Filter
        http.addFilterAt();

// 在这里我们使用的addFilterAt(),将原生用户认证Filter替换为自定义的Filter:
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);

你可能感兴趣的:(前端带你学后端系列,前端,安全,spring,springboot,后端)