Springboot Security 认证鉴权——使用JSON格式参数登录

news2024/12/27 12:30:15

Spring Security 中,默认的登陆方式是以表单形式进行提交参数的。可以参考前面的几篇文章,但是在前后端分离的项目,前后端都是以 JSON 形式交互的。一般不会使用表单形式提交参数。所以,在 Spring Security 中如果要使用 JSON 格式登录,需要自己来实现。那本文介绍两种方式使用 JSON 登录。

  • 方式一:重写 UsernamePasswordAuthenticationFilter 过滤器
  • 方式二:自定义登录接口

1. 通过源码分析可以知道(打断点,往前捋),登录参数的提取在 UsernamePasswordAuthenticationFilter 过滤器中提取的,因此我们只需要模仿UsernamePasswordAuthenticationFilter过滤器重写一个过滤器,替代原有的UsernamePasswordAuthenticationFilter过滤器即可。

UsernamePasswordAuthenticationFilter 的源代码如下:

重写其中的attemptAuthentication方法,默认表单认证,需要修改为兼容json认证 (注意:其中需要定义有参构造方法,且传入authenticationManager类):

  • 1、当前登录请求是否是 POST 请求,如果不是,则抛出异常。
  • 2、判断请求格式是否是 JSON,如果是则走我们自定义的逻辑,如果不是则调用 super.attemptAuthentication 方法,进入父类原本的处理逻辑中;当然也可以抛出异常。
  • 3、如果是 JSON 请求格式的数据,通过 ObjectMapper 读取 request 中的 I/O 流,将 JSON 映射到Map 上。
  • 4、从 Map 中取出 code key的值,判断验证码是否正确,如果验证码有错,则直接抛出异常。如果对验证码相关逻辑感到疑惑,请前往:Spring Security 在登录时如何添加图形验证码验证
  • 5、根据用户名、密码构建 UsernamePasswordAuthenticationToken 对象,然后调用官方的方法进行验证,验证用户名、密码是否真实有效。
package com.cmit.abc.backend.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;


/**
 * 这里只是将用户名/密码的获取方案重新修正下,改为了从 JSON 中获取用户名密码
 */
@Slf4j
// @Component // SecurityConfig中@Bean了
public class JsonUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public JsonUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager){
        super(authenticationManager);
    }

    /* 构造方法和重写set方法,两种方式底层原理都一样。可以点进去看父类的源码*/
    // @Autowired
    // @Override
    // public void setAuthenticationManager(AuthenticationManager authenticationManager) {
    //     super.setAuthenticationManager(authenticationManager);
    // }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 需要是 POST 请求
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        // json类型请求处理方式
        // System.out.println(">>>>>>>>>>>>>>>>>>>>>>>" + request.getContentType());
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
            // Map<String, String> loginData = new HashMap<>();
            ObjectMapper mapper = new ObjectMapper();
            UsernamePasswordAuthenticationToken authRequest = null;
            try (InputStream is = request.getInputStream()) {
                // 通过ObjectMapper读取request中的I/O流,将JSON映射到Map上
                Map<String, String> authenticationBean = mapper.readValue(is, Map.class);
                authRequest = new UsernamePasswordAuthenticationToken(authenticationBean.get("username"), authenticationBean.get("password"));
            } catch (IOException e) {
                e.printStackTrace();
                authRequest = new UsernamePasswordAuthenticationToken("", "");
            } finally {
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            }
        } else {
            // 其他类型的请求依旧走原来的处理方式
            return super.attemptAuthentication(request, response);
        }
    }
}

 2. 配置身份认证管理器SecurityConfig(添加自定义json认证过滤器)

http.addFilterBefore(jsonUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

package com.cmit.abc.backend.config;

import com.cmit.abc.backend.pojo.entity.Permission;
import com.cmit.abc.backend.service.PermissionService;
import com.cmit.abc.backend.service.SecurityAuthenticationHandler;
import com.cmit.abc.backend.service.SecurityUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

/**
 * Security配置类
 * 采用适配器模式,继承后SecurityConfig可以看做是WebSecurityConfigurer
 */
@Configuration
// @EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    SecurityUserDetailsService securityUserDetailsService;

    @Autowired
    SecurityAuthenticationHandler securityAuthenticationHandler;

    @Autowired
    PermissionService permissionService;

    // @Autowired
    // // @Lazy
    // JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    // @Autowired
    // // @Lazy
    // JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter;

    /**
     * 身份安全管理器
     *
     * 使用自定义用户认证
     *
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // auth.userDetailsService(securityUserDetailsService).passwordEncoder(getBCryptPasswordEncoder()); // 默认使用BCrypt,可以不用指定,但是必须@Bean上
        auth.userDetailsService(securityUserDetailsService); // 使用自定义用户认证
    }

    /**
     * Web-Security
     * WebSecurity是包含HttpSecurity的更大的一个概念
     *
     * 放行请求
     *
     */
    @Override
    public void configure(WebSecurity web) {

        // 解决静态资源被拦截的问题(注意不能加prefix,即这里在配置文件配置的全链路前缀:/abc(server.servlet.context-path: /abc))
        web.ignoring().antMatchers("/css/**", "/images/**", "/js/**")
        // 放行微信小程序的相关请求
        .antMatchers("/health/check",
                "/partner/record/save", "/partner/record/info", "/partner/record/type/list", "/partner/record/score/update",
                "/account/wx/**",
                "/company/list");

    }

    /**
     * Http-Security
     * HttpSecurity 是WebSecurity 的一部分
     *
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 获取所有权限数据,需要把“权限表的全部数据”都添加到spring security的缓存中(url <-> tag 二者一一对应匹配放入缓存)
        List<Permission> permissionList = permissionService.findAll();
        // http.authorizeRequests().antMatchers("/health/test").hasAnyAuthority("ADMIN");
        for (Permission p : permissionList) {
            http.authorizeRequests().antMatchers(p.getUrl()).hasAuthority(p.getTag());
        }


        /*
            ① http.httpBasic() // 开启httpBasic认证
                    .and().authorizeHttpRequests().anyRequest().authenticated(); // 配置request请求 -> 任何request请求 -> 需要认证

            ② http.formLogin() // 开启表单认证
                    // .loginPage("/login.html") // 默认,可以不用填写
                    .and().authorizeRequests().anyRequest().authenticated(); // 配置request请求 -> 任何request请求 -> 需要认证
         */

        /**
         * formLogin是表单登录基于session-cookie机制进行用户认证的,
         * 而前后端分离一般使用jwt 即用户状态保存在客户端中,前后端交互通过api接口 无session生成,
         * 所以我们不需要配置formLogin
         *
         * 注意:不开启表单认证,则不会进入loadUserByUsername方法!!
         */
        http
            .formLogin() // 开启表单认证
            // .loginPage("/login.html")
            .loginProcessingUrl("/user/login")// 默认"/login",可以不用填写
            .successHandler(securityAuthenticationHandler)
            .failureHandler(securityAuthenticationHandler)

            .and()
            .logout()
            .logoutUrl("/user/logout")// 默认"/logout",可以不用填写
            .logoutSuccessHandler(securityAuthenticationHandler)

            // 配置拦截规则
            .and()
            .authorizeRequests()
            // 配置/login 为匿名访问 防止被拦截无法进行登录
            //.antMatchers("/account/login").anonymous()// 允许匿名访问
            // 统一挪到configure(WebSecurity web)中了
            // .antMatchers("/health/check", "/partner/record/info", "/partner/record/score/update").permitAll() //放行请求(注意不能加prefix,即这里在配置文件配置的全链路前缀:/abc(server.servlet.context-path: /abc))
            .anyRequest().authenticated() // 配置request请求 -> 任何request请求 -> 需要认证

            // 异常处理器
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(securityAuthenticationHandler)
            .accessDeniedHandler(securityAuthenticationHandler)

            // 自定义过滤器
            .and()
            .addFilter(jwtAuthenticationTokenFilter())
            // .addFilter(jwtAuthenticationTokenFilter);
            // 验证码过滤器放在UsernamePassword过滤器之前
            // .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
            // 添加自定义json认证过滤器
            .addFilterBefore(jsonUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);


        // 关闭csrf(跨站请求伪造)防护
        // 前后端分离项目需要关闭csrf。否则前端和后端的两个域名一般是不一样的,会被拦截。
        http.csrf().disable();

        // 允许cors(跨域)// 前后端分离必配(大部分是因为端口号或域名不同)
        http.cors().configurationSource(corsConfigurationSource());

        // 禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }


    /**
     * 跨域信息配置源
     *
     */
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration(); // 允许跨域的站点
        // corsConfiguration.addAllowedOrigin("*"); // 允许跨域的http方法
        corsConfiguration.addAllowedOriginPattern("*"); // 允许跨域的http方法 // 因为springboot升级成2.4.0以上时对AllowedOrigin设置发生了改变,不能有”*“
        corsConfiguration.addAllowedMethod("*"); // 允许跨域的请求头
        corsConfiguration.addAllowedHeader("*"); // 允许携带凭证
        corsConfiguration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(); // 对所有url都生效
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return urlBasedCorsConfigurationSource;
    }

    /**
     * 默认使用BCrypt,可以不用指定,但是必须@Bean上
     */
    @Bean
    public BCryptPasswordEncoder getBCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
        // 因为security提供的该类并没有考虑前端加密的问题。我们需要重写其matches方法,该方法用于判断从前端接收的密码与数据库中的密码是否一致
        // 我们设前端使用rsa对密码进行加密,后端使用BCrypt对密码进行加密
        // return new SecurityPasswordEncoder();
    }


    /**
     * security登录关键类是 AuthenticationManager认证管理器,
     * 如果我们什么都不配置默认使用的是ProviderManager是ioc运行时注册的bean,我们无法在编译时声明注入到controller,
     * 所以需要我们把默认的ProviderManager在编译期声明为bean
     */
    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    /** 把上面的super.authenticationManager()传入封装到自定义过滤器中了 */
    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() throws Exception {
        JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter = new JwtAuthenticationTokenFilter(authenticationManager());
        return jwtAuthenticationTokenFilter;
    }

    /**
     * json 格式认证支持
     *
     * 当我们替换了 UsernamePasswordAuthenticationFilter 之后,原本在 SecurityConfig#configure 方法中关于 form 表单的配置就会失效,
     * 那些失效的属性,都可以在配置 JsonUsernamePasswordAuthenticationFilter 实例的时候配置;还需要记得配置AuthenticationManager,否则启动时会报错。
     *
     */
    /** 把上面的super.authenticationManager()传入封装到自定义过滤器中了 */
    @Bean
    public JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter() throws Exception {
        JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter = new JsonUsernamePasswordAuthenticationFilter(authenticationManager());
        // 配置采用json形式认证时的各种回调处理器
        jsonUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(securityAuthenticationHandler);
        jsonUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(securityAuthenticationHandler);
        jsonUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/user/login");// logout的url就不用设置了,使用上面的表单方式中的配置
        return jsonUsernamePasswordAuthenticationFilter;
    }

}

 3.自定义各种Handler处理器

package com.cmit.abc.backend.service;

import com.alibaba.fastjson.JSONObject;
import com.cmit.abc.backend.pojo.dto.ResponseDTO;
import com.cmit.abc.backend.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
 * 自定义各种Handler处理器
 *
 * 登录成功或失败处理器,退出登录处理器等
 */

@Slf4j
@Component
public class SecurityAuthenticationHandler
        implements AuthenticationSuccessHandler, AuthenticationFailureHandler,
        LogoutSuccessHandler,
        AccessDeniedHandler,
        AuthenticationEntryPoint {
        // implements SavedRequestAwareAuthenticationSuccessHandler, SimpleUrlAuthenticationFailureHandler, SimpleUrlLogoutSuccessHandler {
        // extends SavedRequestAwareAuthenticationSuccessHandler {

    // // 这个工具类,可以方便的帮助我们跳转页面
    // RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    JwtUtils jwtUtils;


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        // System.out.println("登录成功后,继续处理.............");
        // 重定向到index页面
        // redirectStrategy.sendRedirect(request, response, "/");

        String token = jwtUtils.generateToken(((User)authentication.getPrincipal()).getUsername());

        this.responseWrapper(response, HttpStatus.OK.value(), "登录成功!", Map.of(jwtUtils.getHeader(), token));
    }

    /**
     * 登录失败后的处理逻辑
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        // System.out.println("登录失败后继续处理...............");
        // 重定向到login页面
        // redirectStrategy.sendRedirect(request, response, "/toLoginPage");

        // 登录失败
        String message = "";
        if (exception instanceof BadCredentialsException) {
            message = "用户名或者密码输入错误,请重新输入!";
        } else if(exception instanceof LockedException) {
            message = "账户被锁定,请联系管理员!";
        }
        this.responseWrapper(response, HttpStatus.UNAUTHORIZED.value(), message, Map.of("exception", exception.getMessage()));
    }

    /**
     * 登出处理器
     *
     * ① 在用户退出登录时,我们需将原来的JWT置为空返给前端
     *      - 这样前端会将空字符串覆盖之前的jwt,JWT是无状态化的,销毁JWT是做不到的,JWT生成之后,只有等JWT过期之后,才会失效。因此我们采取置空策略来清除浏览器中保存的JWT。
     * ② 同时我们还要将我们之前置入SecurityContext中的用户信息进行清除
     *      - 这可以通过创建SecurityContextLogoutHandler对象,调用它的logout方法完成
     *
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        // System.out.println("退出之后继续处理。。。");
        // redirectStrategy.sendRedirect(request, response, "/login");
        if(authentication != null) {
            new SecurityContextLogoutHandler().logout(request, response, authentication);
        }

        //置空token
        this.responseWrapper(response, HttpStatus.OK.value(), "退出成功!", Map.of(jwtUtils.getHeader(), ""));

    }

    /**
     * 无权限访问的处理
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        this.responseWrapper(response, HttpStatus.FORBIDDEN.value(), "您无访问权限!",  Map.of("exception", accessDeniedException.getMessage()));
    }

    /**
     * 当BasicAuthenticationFilter(即JwtAuthenticationTokenFilter)认证失败的时候(token校验抛异常,无权限,且不在白名单中),会进入AuthenticationEntryPoint
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        log.error(authException.getMessage());
        // 登录认证失败
        this.responseWrapper(response, HttpStatus.UNAUTHORIZED.value(), "您未登录,请先登录!", Map.of("exception", authException.getMessage()));

    }


    private void responseWrapper(HttpServletResponse response, int state, String message, Map<String, Object> properties) throws IOException {
        // 设置MIME,务必在getWriter()方法被调用之前调用
        // response.setContentType("application/json;charset=UTF-8");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setStatus(state);

        // // Map<Object, Object> result = new HashMap<>();
        // JSONObject resultObj = new JSONObject();
        // resultObj.put("state", state);
        // resultObj.put("message", message);
        // Optional.ofNullable(properties).ifPresent(p -> resultObj.putAll(p));
        // response.getWriter().write(resultObj.toString());

        ResponseDTO<Map<String, Object>> res = ResponseDTO.response(state, message, properties);
        response.getWriter().write(JSONObject.toJSONString(res));

    }
}

补充: 

4. 自定义JwtFilter过滤器

package com.cmit.abc.backend.config;

import com.cmit.abc.backend.service.SecurityUserDetailsService;
import com.cmit.abc.backend.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 验证jwt,获取username,设置到上下文(SecurityContextHolder)
 * 这样后续我们就能通过调用SecurityContextHolder.getContext().getAuthentication().getPrincipal()等方法获取到当前登录的用户信息了。
 */
@Slf4j
// @Component // SecurityConfig中@Bean了
public class JwtAuthenticationTokenFilter extends BasicAuthenticationFilter {

    @Autowired
    SecurityUserDetailsService securityUserDetailsService;

    @Autowired
    private JwtUtils jwtUtils;

    public JwtAuthenticationTokenFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取请求头中的token
        String token = request.getHeader(jwtUtils.getHeader());

        // 这里如果没有jwt,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以是可以放行的
        // 没有jwt相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口
        if (Strings.isNotBlank(token)) {
            // 验证token
            Claims claims = jwtUtils.parseJwtToken(token);
            if (claims==null) {
                throw new JwtException("token验证不通过");
            }
            if (jwtUtils.isTokenExpired(claims.getExpiration())) {
                throw new JwtException("token已过期");
            }

            // //实际项目中可以把登录成功的用户实体保存到redis通过token取到填充即可 (这里为了简便我直接硬编码了)
            String username = claims.getSubject();
            // 获取用户的权限等信息
            UserDetails userDetails = securityUserDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), null, userDetails.getAuthorities());

            //将认证通过的信息填充到安全上下文中(用于一次请求的授权)
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        filterChain.doFilter(request, response);
    }
}

5. 自定义一个UserDetails类,“实现”自Spring Security提供的UserDetailsService接口

package com.cmit.abc.backend.service;

import com.cmit.abc.backend.pojo.entity.Permission;
import com.cmit.abc.backend.pojo.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;


/**
 * 基于数据库完成认证
 *
 * 两种场景,会调用该方法:
 *      ① 访问/login(登录)
 *      ② 访问其他链接(携带token访问)
 *
 */
@Service
public class SecurityUserDetailsService implements UserDetailsService {

    @Autowired
    UserService userService;

    @Autowired
    PermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {


        /**
         * ① 查找用户
         */
        User user = userService.findByUserName(username);
        if(user == null){
            throw new UsernameNotFoundException("用户名没找到:"+username);
        }

        /**
         * ② 权限的集合
         * 正常这里应该传递权限集合。由于我们还没有权限,所以先声明一个“空的”权限集合供new User()的参数使用着(因为构造方法里面不能传入null,所以伪装一下哈哈)
         */
        // Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
        Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
        // ==================================模拟数据库中获取权限=========================================
        // if (username.equalsIgnoreCase("admin")) {
        //     authorities.add(new SimpleGrantedAuthority("ADMIN"));
        // }else {
        //     authorities.add(new SimpleGrantedAuthority("MEMBER"));
        // }
        // ==================================模拟数据库中获取权限=========================================
        List<Permission> permissionList = permissionService.findByUserId(user.getId());
        permissionList.stream().forEach(p -> authorities.add(new SimpleGrantedAuthority(p.getTag())));

        UserDetails userDetails = new org.springframework.security.core.userdetails.User(
                username,
                // "{bcrypt}" + user.getPassword(), // noop表示不使用密码加密,bcrypt表示使用bcrypt作为加密算法
                // true, // 启用账号
                // true, // 账号未过期
                // true, // 用户凭证是否过期
                // true, // 用户是否锁定
                user.getPassword(),
                authorities
        );

        return userDetails;
    }


    /**
     *
     * BCrypt 强哈希算法
     * 单向加密算法,相对于MD5等加密方式更加安全(BCrypt加密算法:经过salt和cost的处理,加密时间(百ms级)远远超过md5(大概1ms左右))
     *
     */
    // 测试BCrypt加密方法
    public static void main(String[] args) {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String encode = bCryptPasswordEncoder.encode("123456");
        String encode1 = bCryptPasswordEncoder.encode("123456");
        // 对比发现,可以两次加密结果不一样
        System.out.println(encode + " <<<>>> " + encode1);
        boolean matchesBoolean = bCryptPasswordEncoder.matches("123456", encode);
        System.out.println(matchesBoolean);

        /*

        bcrypt加密后的字符串形如: 
            $2a$10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq
            其中$是分割符,无意义;2a是bcrypt加密版本号;10是const的值;而后的前22位是salt值;再然后的字符串就是密码的密文了;这里的const值即生成salt的迭代次数,默认值是10,推荐值12。

         */
    }

}

6. permissionRepository.findByUserId(userId)的sql语句(JPA编写)

@Query(value = "select * from permission p, role_permission rp, role r, user_role ur, user u " +
        "WHERE p.id = rp.permission_id and rp.role_id = r.id and r.id = ur.role_id and ur.user_id = u.id and u.id = :userId", nativeQuery = true)
List<Permission> findByUserId(long userId);
package com.cmit.abc.backend.pojo.entity;


import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.*;
import java.io.Serializable;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@DynamicUpdate
@DynamicInsert
@Entity
//@EqualsAndHashCode//不能用@EqualsAndHashCode和@ToString,否则死循环内存溢出
@Table(name = "permission")
public class Permission implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "tag")
    private String tag;

    @Column(name = "url")
    private String url;

    @Column(name = "status")
    private Integer status;
}

 


参考链接:

四:Spring Security 登录使用 JSON 格式数据_爱是与世界平行的博客-CSDN博客

Spring Security 使用JSON格式参数登录的两种方式 - 掘金 (juejin.cn)

(943条消息) SpringSecurity报错authenticationManager must be specified_小楼夜听雨QAQ的博客-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/506431.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ansys Lumerical | 单行载流子光电探测器仿真方法

综述 在本例中&#xff0c;我们将研究混合硅基光电探测器的各项性能。单行载流子&#xff08;uni-traveling carrier&#xff0c;UTC&#xff09;光电探测器&#xff08;PD&#xff09;由InP/InGaAs制成&#xff0c;其通过渐变耦合的方式与硅波导相连。在本次仿真中&#xff0c…

04-Docker镜像

镜像 镜像是一种轻量级、可执行的独立软件包&#xff0c;它包含运行某个软件所需的所有内容&#xff0c;我们把应用程序和配置依赖打包好形成一个可交付的运行环境&#xff08;包括代码、运行时需要的库、环境变量和配置文件等&#xff09;&#xff0c;这个打包好的运行环境就是…

上海亚商投顾:沪指午后跳水跌超1% 两市超4000只个股下跌

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 市场情绪 沪指今日冲高回落&#xff0c;盘中一度站上3400点关口&#xff0c;午后跳水跌超1%&#xff0c;深成指、创业板指同…

本科生学数据分析转行,能学会吗?

当然可以&#xff0c;大专及以上都可以学习数据分析转行&#xff0c;本科学历学习更有优势。数据分析职业对于学历方面还是比较看重的&#xff0c;同样技能情况下&#xff0c;学历越高&#xff0c;入行薪资起点也会高个至少一两千&#xff1b;入行以后的升职加薪就看个人的能力…

vue3学习七 toRef 和 toRefs

toRef 和 ref 的作用差不多是一样的&#xff0c;都是可以把一个数据变成响应式的 我们一般使用toRef 或都 toRefs 是用来&#xff0c;简化 template 中的 数据的写法的&#xff0c; 使一个深层次的数据不用书写的时候那么麻烦。 如果不嫌麻烦的话&#xff0c; 这两个api 可以不…

尚硅谷-宋红康-JVM上中下篇完整笔记-JVM中篇

一.Class文件结构 1.概述 1.1 字节码文件的跨平台性 所有的JVM全部遵守Java虚拟机规范:Java SE Specifications&#xff0c;也就是说所有的JV环境都是一样的&#xff0c;这样一来字节码文件可以在各种JVM上运行。 1.2 Java的前端编译器 想要让一个Java程序正确地运行在JVM中&am…

安装Nacos

什么是Nacos 官网中如此说道&#xff1a; Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集&#xff0c;帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。 Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以…

CSS选择器的常见用法

文章目录 CSS是什么CSS的引入方式内部样式表行内样式表外部样式 选择器基础选择器类选择器id选择器通配符选择器 复合选择器后代选择器 CSS是什么 CSS就是&#xff08;Cascading Style Sheets&#xff09;就是层叠样式表&#xff0c;CSS 能够对网页中元素位置的排版进行像素级…

从项目到技能,软件测试面试高频题总结 (附答案),收割10个offer...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 软件测试面试题简历…

农村供水调度系统在河北某地的建设案例

项目背景 农村饮水安全事关广大农村居民的切身利益&#xff0c;是脱贫攻坚、乡村振兴的基础条件。该县为加快推进农村人饮安全运行管理工作&#xff0c;建立健全运管服务体系&#xff0c;改善当前农村人饮安全运营现状&#xff0c;积极实施城乡供水一体化工程&#xff0c;进一步…

八、使用代码对道路结果进行后处理及iou优化步骤详解

老师又给我画了大饼 没办法 只能按照他们的想法做个尝试 上一篇的方法还没进行下去 就被叫停 又更新了一个新的想法 这里记录一下 我的尝试过程 一、图片膨胀 首先使用代码对道路进行膨胀 这里的代码 import cv2 import numpy as np img cv2.imread(gt_dirname, 0) ke…

李白、高适、杜甫,情义深深,抵不过乱世游离

李白&#xff0c;字太白&#xff0c;是唐朝浪漫主义诗人&#xff0c;被后人誉为“诗仙”&#xff0c;杜甫&#xff0c;字子美&#xff0c;唐代现实主义诗人&#xff0c;李白和杜甫合称为“李杜”。高适&#xff0c;字达夫&#xff0c;唐代诗人。李白&#xff0c;杜甫&#xff0…

JVM学习随笔03——Java堆中new一个对象的步骤

目录 一、进行类加载 二、堆中分配内存 1、怎么输出GC日志&#xff1a; 2、内存分配的两种方式&#xff1a; 3、内存分配过程中并发控制的两种方式&#xff1a; 三、内存空间初始化 四、对象头初始化&#xff08;对象头包含哪些信息&#xff1f;&#xff09; 五、执行构…

【C++】-const对象及成员函数之类和对象中篇完结(中)

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树 ❤️‍&#x1fa79;作者宣言&#xff1a;认真写好每一篇博客 &#x1f4a8;作者gitee:gitee &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 文章目录 前言一、案例的引入二、const对象和成员函数三、取地…

有效库存管理策略:避免滞销和短缺的利器!

在现代企业管理中&#xff0c;采购计划和库存管理是非常重要的环节。一方面&#xff0c;采购计划可以让企业根据市场需求和自身情况&#xff0c;科学合理地安排采购时间、采购量和采购方式&#xff0c;从而有效地控制成本&#xff0c;并确保生产和销售的顺畅&#xff1b;另一方…

bigdata-file-viewer--大数据文件查看工具

bigdata-file-viewer--大数据文件查看工具 bigdata-file-viewer是什么常用功能安装 bigdata-file-viewer是什么 一个跨平台&#xff08;Windows&#xff0c;MAC&#xff0c;Linux&#xff09;桌面应用程序&#xff0c;用于查看常见的大数据二进制格式&#xff0c;例如Parquet&…

【C++ 学习 ③】- 类的六大默认成员函数

目录 一、 构造函数 1.1 - 概念 1.2 - 特性 二、析构函数 2.1 - 概念 2.2 - 特性 2.3 - 用栈实现队列 三、拷贝构造函数 四、运算符重载 4.1 - 双目运算符 4.2 - 单目运算符 4.3 - 赋值运算符重载 五、const 成员函数 六、取地址 和 const 取地址运算符重载 参考…

如何写软件测试简历项目经验,靠这个面试都要赶场

一、前言&#xff1a;浅谈面试 面试是我们进入一个公司的门槛&#xff0c;通过了面试才能进入公司&#xff0c;你的面试结果和你的薪资是息息相关的。那如何才能顺利的通过面试&#xff0c;得到公司的认可呢?面试软件测试要注意哪些问题呢?下面和笔者一起来看看吧。这里分享一…

如何计算旋转框的IOU

一、先将两个框转换为角点形式 一般我们得到的是中心点&#xff0c;宽高&#xff0c;和旋转角度。通过矢量旋转公式得到角点形式。 二、判断四个角点是否在对方的框里&#xff0c;并保存在里面的角点 A的角点向B的相邻的两条边投影(任意的B的两条边)&#xff0c;使用向量点积得…

微信开发者工具实现代码加固

一&#xff1a;下载安装node.js node.js下载地址&#xff1a;下载 | Node.js 二&#xff1a;微信开发者工具安装代码加固拓展 1&#xff1a;开发者工具选择设置-》拓展设置 2:安装代码加固拓展 三&#xff1a;使用代码加固拓展实现核心密码加密 1&#xff1a;安装devtool-cod…