Spring Boot + Spring Security + JWT + 微信小程序登录

Spring Boot + Spring Security + JWT + 微信小程序登录整合教程

参考文章

文章目录

  • 整合思想
  • 整合步骤
    • 1. AuthenticationToken
    • 2. AuthenticationProcessingFilter
    • 3. AuthenticationManager
    • 4. JWTFilter
    • 5. AuthenticationEntryPoint
    • 6. AuthenticationSuccessHandler
    • 7. 全局异常捕获
    • 8. WebSecurityConfig
    • 9. 开启全局方法安全

整合思想

  1. 自定义AuthenticationToken,封装登录的相关信息,用于SecurityContext存储和验证过程
  2. 自定义AuthenticationProcessingFilter,代替默认的UsernamePasswordAuthenticationFilter
    1. 使用code获取open_id、session_key等
    2. 将获取到的open_id、session_key等封装成一个未认证的AuthenticationToken,传递到AuthenticationManager
  3. AuthenticationManager执行认证授权过程
    1. 查询数据库,获取用户信息
    2. 没有用户则写入用户
    3. 使用用户信息获取角色
    4. 封装授权信息
    5. 将相关信息封装成一个已认证的AuthenticationToken,这个AuthenticationToken会传递到AuthenticationSuccessHandler(认证成功的处理方法)
  4. 配置Spring Security配置,将UsernamePasswordAuthenticationFilter替换成自定义的AuthenticationProcessingFilter
  5. 在自定义的AuthenticationProcessingFilter之前添加JWTFilter

整合步骤

1. AuthenticationToken

用于存储微信小程序登录信息

@Getter
@Setter
@ToString
public class WxAppletAuthenticationToken extends AbstractAuthenticationToken {
    private String openId;
    private Long userId;
    private String sessionKey;
    private String rawData;
    private String signature;
    private String role;

    // 使用openid和sessionKey创建一个未验证的token
    public WxAppletAuthenticationToken(String openId, String sessionKey, String role) {
        super(null);
        this.openId = openId;
        this.sessionKey = sessionKey;
        this.role = role;
    }

    // 使用openid和sessionKey创建一个已验证的token
    public WxAppletAuthenticationToken(String openId, String sessionKey, Long userId, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.openId = openId;
        this.userId = userId;
        this.sessionKey = sessionKey;
        super.setAuthenticated(true);
    }

    // 使用openid创建一个已验证的token
    public WxAppletAuthenticationToken(String openId, Long userId, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.openId = openId;
        this.userId = userId;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return this.openId;
    }

    @Override
    public Object getPrincipal() {
        return this.sessionKey;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

2. AuthenticationProcessingFilter

用于匹配的请求

@Slf4j
public class WxAppletAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public WxAppletAuthenticationFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
        String method = httpServletRequest.getMethod().toUpperCase();
        if (!"POST".equals(method)) {
            throw new UnSupportMethodException(method);
        }
        String contentType = httpServletRequest.getContentType().toLowerCase();
        if ("application/json;charset=utf-8".equals(contentType)) {
            throw new UnSupportContentTypeException(contentType);
        }
        // body参数转换为json
        StringBuffer sb = new StringBuffer();
        String line = null;
        BufferedReader reader = httpServletRequest.getReader();
        while ((line = reader.readLine()) != null)
            sb.append(line);

        String jsonString = sb.toString().replaceAll("\\s", "").replaceAll("\n", "");
        JSONObject jsonObject = JSONUtil.parseObj(jsonString);

        // 取出code
        String code = jsonObject.get("code", String.class);
        if (ObjectUtil.isEmpty(code)) {
            throw new MissingParameterException("code");
        }
        String role = jsonObject.get("role", String.class);
        if (ObjectUtil.isEmpty(role)) {
            throw new MissingParameterException("role");
        }
        JSONObject session = WeChatUtils.code2Session(code);

        String openId = session.get("openid", String.class);
        String sessionKey = session.get("session_key", String.class);

        if (ObjectUtil.isEmpty(openId) || ObjectUtil.isEmpty(sessionKey)) {
            throw new RuntimeException("无法获取openId");
        }

        return this.getAuthenticationManager().authenticate(new WxAppletAuthenticationToken(openId, sessionKey , role));
    }
}

3. AuthenticationManager

真正执行认证逻辑的manager

@Slf4j
@Component
public class WxAppletAuthenticationManager implements AuthenticationManager {

    @Resource
    private UserService userService;

    @Resource
    private UserRoleService userRoleService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        WxAppletAuthenticationToken token = null;
        if (authentication instanceof WxAppletAuthenticationToken) {
            token = (WxAppletAuthenticationToken) authentication;
        }
        User user = userService.getByOpenId(token.getOpenId());
        if (ObjectUtil.isEmpty(user)) {
            // 写入角色
            Integer roleId = RoleUtils.getRoleId(token.getRole());
            if (ObjectUtil.isEmpty(roleId)) {
                // 参数的角色不在列表中
                throw new RuntimeException("注册失败:" + token.toString());
            }
            // 注册账号
            user = userService.registry(token.getOpenId(), roleId.intValue());
            if (ObjectUtil.isEmpty(user)) {
                // 注册失败
                throw new RuntimeException("注册失败:" + token.toString());
            }
        }
        // 获取权限
        List<UserRoleVO> userRoleVOList = userRoleService.getByUserId(user.getUserId());
        List<SimpleGrantedAuthority> authorityList = userRoleVOList
                .stream()
                .map(userRoleVO -> new SimpleGrantedAuthority("ROLE_" + userRoleVO.getRoleName()))
                .collect(Collectors.toList());
        return new WxAppletAuthenticationToken(user.getOpenId(), user.getUserId(), authorityList);
    }
}

4. JWTFilter

在AuthenticationProcessingFilter之前,先验证客户端的token

@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private UserService userService;

    @Resource
    private UserRoleService userRoleService;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String token = httpServletRequest.getHeader("Authorization");
        if (!ObjectUtil.isEmpty(token)) {
            // 从token中获取openId和userId
            String openId = (String) JwtUtils.getClaim(token, "openId");
            Long userId = (Long) JwtUtils.getClaim(token, "userId");

            if (!ObjectUtil.isEmpty(userId) && !ObjectUtil.isEmpty(openId) && SecurityContextHolder.getContext().getAuthentication() == null) {
                log.info("获取" + userId + "的角色");
                List<UserRoleVO> userRoleVOList = userRoleService.getByUserId(userId);
                List<SimpleGrantedAuthority> authorityList = userRoleVOList
                        .stream()
                        .map(userRoleVO -> new SimpleGrantedAuthority("ROLE_" + userRoleVO.getRoleName()))
                        .collect(Collectors.toList());

                // 将token加入安全上下文
                SecurityContextHolder.getContext().setAuthentication(new WxAppletAuthenticationToken(openId, userId, authorityList));
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

5. AuthenticationEntryPoint

未登录时是处理端点

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletRequest.getRequestDispatcher("/error/auth").forward(httpServletRequest, httpServletResponse);
    }
}

6. AuthenticationSuccessHandler

登陆成功后的处理器

public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {

        WxAppletAuthenticationToken authenticationToken = (WxAppletAuthenticationToken) authentication;

        Map<String, Object> data = new HashMap<>();
        data.put("userId", authenticationToken.getUserId());
        data.put("openId", authenticationToken.getOpenId());

        // 写回token
        String token = JwtUtils.getToken(data);
        httpServletResponse.setContentType(ContentType.JSON.getValue());
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.getWriter().write(JSONUtil.parseObj(new LoginVO(token)).toStringPretty());
    }
}

7. 全局异常捕获

@RestControllerAdvice
@Order(1)
public class GlobalExceptionHandler {

    /**
     * 禁止访问
     */
    @ExceptionHandler(AccessDeniedException.class)
    public Result accessDeniedException() {
        return Result.error(ResultCode.NO_PERMISSION);
    }
}

8. WebSecurityConfig

Spring Security配置

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
            .disable()
            .sessionManagement()
            // 不创建Session, 使用jwt来管理用户的登录状态
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/error/**", "/swagger-ui.html/**", "/webjars/**", "/v2/**", "/swagger-resources/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(new CustomAuthenticationEntryPoint());

        http
            .addFilterAt(getWxAppletAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .addFilterBefore(getJwtAuthenticationTokenFilter(), WxAppletAuthenticationFilter.class);
        ;
    }

    @Resource
    private WxAppletAuthenticationManager wxAppletAuthenticationManager;

    @Bean
    public WxAppletAuthenticationFilter getWxAppletAuthenticationFilter() {
        WxAppletAuthenticationFilter wxAppletAuthenticationFilter = new WxAppletAuthenticationFilter("/login");
        wxAppletAuthenticationFilter.setAuthenticationManager(wxAppletAuthenticationManager);
        wxAppletAuthenticationFilter.setAuthenticationSuccessHandler(getCustomAuthenticationSuccessHandler());
        return wxAppletAuthenticationFilter;
    }

    @Bean
    public CustomAuthenticationSuccessHandler getCustomAuthenticationSuccessHandler() {
        return new CustomAuthenticationSuccessHandler();
    }

    @Bean
    public JwtAuthenticationTokenFilter getJwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter();
    }
}

9. 开启全局方法安全

@Bean
public CustomAuthenticationSuccessHandler getCustomAuthenticationSuccessHandler() {
return new CustomAuthenticationSuccessHandler();
}

@Bean
public JwtAuthenticationTokenFilter getJwtAuthenticationTokenFilter() {
    return new JwtAuthenticationTokenFilter();
}

}


## 9. 开启全局方法安全

> 在启动类上加上注解@EnableGlobalMethodSecurity(prePostEnabled = true)

你可能感兴趣的:(java,java)