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

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博客

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在Spring Boot中实现微信登录鉴权,可以使用Spring Security框架来实现。具体步骤如下: 1.引入相关依赖 ```xml <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>5.3.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>5.3.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> <version>5.3.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> <version>5.3.4.RELEASE</version> </dependency> ``` 2.配置Spring Security ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomOAuth2UserService customOAuth2UserService; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/error", "/webjars/**").permitAll() .anyRequest().authenticated() .and() .oauth2Login() .userInfoEndpoint() .userService(customOAuth2UserService); } } ``` 3.实现CustomOAuth2UserService ```java @Service public class CustomOAuth2UserService extends DefaultOAuth2UserService { @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { // 获取用户信息 OAuth2User oAuth2User = super.loadUser(userRequest); // 获取用户的唯一标识 String openId = oAuth2User.getAttribute("openid"); // TODO 根据openId查询用户信息并返回 return oAuth2User; } } ``` 4.配置application.yml ```yaml spring: security: oauth2: client: registration: wechat: client-id: your-client-id client-secret: your-client-secret scope: snsapi_login redirect-uri: http://localhost:8080/login/oauth2/code/wechat client-name: WeChat provider: wechat: authorization-uri: https://open.weixin.qq.com/connect/qrconnect token-uri: https://api.weixin.qq.com/sns/oauth2/access_token user-info-uri: https://api.weixin.qq.com/sns/userinfo user-name-attribute: openid ``` 以上是在Spring Boot中实现微信登录鉴权的基本步骤,具体实现还需要根据实际情况进行调整。如果需要实现微信支付功能,可以参考微信支付接口说明进行开发。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值