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

这篇具有很好参考价值的文章主要介绍了Springboot Security 认证鉴权——使用JSON格式参数登录。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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

UsernamePasswordAuthenticationFilter 的源代码如下:

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

重写其中的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博客文章来源地址https://www.toymoban.com/news/detail-441269.html

到了这里,关于Springboot Security 认证鉴权——使用JSON格式参数登录的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java21 + SpringBoot3使用Spring Security时如何在子线程中获取到认证信息

    目录 前言 原因分析 解决方案 方案1:手动设置线程中的认证信息 方案2:使用 DelegatingSecurityContextRunnable 创建线程 方案3:修改 Spring Security 安全策略 通过设置JVM参数修改安全策略 通过 SecurityContextHolder 修改安全策略 总结 近日心血来潮想做一个开源项目,目标是做一款可以适

    2024年02月19日
    浏览(37)
  • 系列十一、Spring Security登录接口兼容JSON格式登录

            前后端分离中,前端和后端的数据交互通常是JSON格式,而Spring Security的登录接口默认支持的是form-data或者x-www-form-urlencoded的,如下所示: 那么如何让Spring Security的登录接口也支持JSON格式登录呢?请看下文分析 

    2024年01月20日
    浏览(40)
  • 【Spring Security】分布式鉴权的使用

    🎉🎉欢迎来到我的CSDN主页!🎉🎉 🏅我是Java方文山,一个在CSDN分享笔记的博主。📚📚 🌟推荐给大家我的专栏《Spring Security》。🎯🎯 👉点击这里,就可以查看我的主页啦!👇👇 Java方文山的个人主页 🎁如果感觉还不错的话请给我点赞吧!🎁🎁 💖期待你的加入,一

    2024年02月02日
    浏览(43)
  • requests库post请求参数data、json和files的使用,postman的各种数据格式

    application/x-www-form-urlencoded 和 application/json 是两种不同的 HTTP 请求体格式,它们在 Python 中的处理方式也不同。 application/x-www-form-urlencoded 是 Web 表单默认的提交方法,浏览器会将表单数据编码为 key-value 键值对,并将其放在请求体中。该格式数据可以通过 Python 中的标准库 url

    2024年02月16日
    浏览(65)
  • 【Spring Boot Admin】使用(整合Spring Security服务,添加鉴权)

    Spring Boot Admin 监控平台 背景:Spring Boot Admin 监控平台不添加鉴权就直接访问的话,是非常不安全的。所以在生产环境中使用时,需要添加鉴权,只有通过鉴权后才能监控客户端服务。本文整合Spring Security进行实现。 pom依赖 yml配置 启动类@EnableAdminServer 安全配置类:SecuritySe

    2024年02月16日
    浏览(34)
  • 新增:前端提示“请求JSON参数格式不正确,请检查参数格式

    解决方法: 1.检查后端控制台报错: 参数格式传递异常,请求号为:3b44424d-73bd-4db7-970b-38638451c439,具体信息为:JSON parse error: Cannot deserialize value of type ` java.lang.Long` from String \\\" 新增的字段 \\\": not a valid Long value; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot

    2024年02月17日
    浏览(38)
  • SpringBoot2.3集成Spring Security(二) JWT认证

    紧接上文,我们已经完成了 SpringBoot中集成Spring Security,并且用户名帐号和密码都是从数据库中获取。但是这种方式还是不能满足现在的开发需求。 使用JWT的好处: 无状态认证:JWT本身包含了认证信息和声明,服务器不需要在会话中保存任何状态。这样使得应用程序可以更加

    2024年02月11日
    浏览(58)
  • Springboot +spring security,自定义认证和授权异常处理器

    在Spring Security中异常分为两种: AuthenticationException 认证异常 AccessDeniedException 权限异常 我们先给大家演示下如何自定义异常处理器,然后再结合源码帮助大家进行分析 如何创建一个SpringSecurity项目,前面文章已经有说明了,这里就不重复写了。 3.1配置SecurityConfig 这里主要是

    2024年02月07日
    浏览(42)
  • SpringBoot小项目——简单的小区物业后台管理系统 & 认证鉴权 用户-角色模型 & AOP切面日志 & 全局异常【源码】

    基于SpringBoot的简单的小区物业后台管理系统,主要功能有报修的处理,楼宇信息和房屋信息的管理,业主信息的管理【核心】,以及数据统计分析模块Echarts绘图;此外采用用户-角色权限模型,结合自定义注解实现简单的权限管理功能,采用aop切面实现日志的存储,全局异常

    2024年02月06日
    浏览(50)
  • 使用Token方式实现用户身份鉴权认证

    Token,也称为“令牌”,是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。比如如下形式: 39faf62271944fe48c4f1d69be71bc9a 传

    2024年02月11日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包