OpenAPI鉴权(二)jwt鉴权

一、思路

前端调用后端可以使用jwt鉴权;调用三方接口也可以使用jwt鉴权。对接多个三方则与每个third parth都约定一套token规则,因为如果使用同一套token,token串用可能造成权限越界问题,且payload交叉业务不够清晰。下面的demo包含了两套jwt,前端和一个三方(openApi)的:

1、token生成:

(1)签发给前端的token在本项目生成;

(2)签发给第三方的token,由第三方根据双方约定的算法、密钥和payload通信信息自己生成

。不能调用本项目(被调用方)接口生成,否则这个生成token的接口需要加白名单,

会造成接口攻击和token泄露的安全问题。

2、token校验:

先判断是哪个业务的token,再用各自约定的算法和业务规则校验。这里是根据url来判断的,

不能只根据audience来判断,如用前端的token访问open api接口,从token解析出audience

是前端的,再用前端的规则校验,校验通过访问成功;

二、demo

1、pom与配置文件
复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>openapi-ta</artifactId>
        <groupId>us.zoom.openapi</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>openapi-ta-mas</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <ta.product.name>Mas</ta.product.name>
    </properties>

    <dependencies>
        <dependency>
            <groupId>us.zoom.openapi</groupId>
            <artifactId>openapi-ta-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>us.zoom.openapi.mas</groupId>
            <artifactId>open-api-mas-common</artifactId>
            <version>1.0.1</version>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.1.8</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.13.0</version>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                    <skip>true</skip>
                    <executable>false</executable>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${surefire.version}</version>
                <configuration>
                    <systemPropertyVariables>
                        <org.uncommons.reportng.escape-output>false</org.uncommons.reportng.escape-output>
                        <spring.profiles.active>${spring.profiles.active}</spring.profiles.active>
                        <ta.product.name>${ta.product.name}</ta.product.name>
                    </systemPropertyVariables>
                    <skip>false</skip>
                    <testFailureIgnore>false</testFailureIgnore>
                    <argLine>
                        -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
                        --add-opens java.base/java.lang=ALL-UNNAMED
                    </argLine>
                    <suiteXmlFiles>
                        <suiteXmlFile>${project.basedir}/src/main/resources/suites/${spring.profiles.active}/AlertRuleAPIBvtSuite.xml</suiteXmlFile>
                    </suiteXmlFiles>
                    <properties>
                        <usedefaultlisteners>false</usedefaultlisteners>
                    </properties>
                </configuration>

            </plugin>
        </plugins>


        <resources>

            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                    <include>application.yml</include>
                    <include>application-${spring.profiles.active}.yml</include>
                </includes>
            </resource>

            <resource>
                <directory>src/main/java</directory>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>

    <profiles>
        <profile>
            <id>dev</id>
            <activation>
                <!--default env-->
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <spring.profiles.active>dev</spring.profiles.active>
            </properties>
        </profile>
        <profile>
            <id>go</id>
            <properties>
                <spring.profiles.active>go</spring.profiles.active>
            </properties>
        </profile>
        <profile>
            <id>aw1</id>
            <properties>
                <spring.profiles.active>aw1</spring.profiles.active>
            </properties>
        </profile>
        <profile>
            <id>us02</id>
            <properties>
                <spring.profiles.active>us02</spring.profiles.active>
            </properties>
        </profile>
        <profile>
            <id>us03</id>
            <properties>
                <spring.profiles.active>us03</spring.profiles.active>
            </properties>
        </profile>
        <profile>
            <id>us04</id>
            <properties>
                <spring.profiles.active>us04</spring.profiles.active>
            </properties>
        </profile>
        <profile>
            <id>us05</id>
            <properties>
                <spring.profiles.active>us05</spring.profiles.active>
            </properties>
        </profile>
        <profile>
            <id>us06</id>
            <properties>
                <spring.profiles.active>us06</spring.profiles.active>
            </properties>
        </profile>
        <profile>
            <id>us07</id>
            <properties>
                <spring.profiles.active>us07</spring.profiles.active>
            </properties>
        </profile>
        <profile>
            <id>us01</id>
            <properties>
                <spring.profiles.active>us01</spring.profiles.active>
            </properties>
        </profile>
    </profiles>

</project>

server.port=6666
server.servlet.context-path=/jwtDemo
2、启动类
复制代码
package com.demo.security;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.jmx.support.RegistrationPolicy;

@SpringBootApplication
//解决报错MXBean already registered with name org.apache.commons.pool2:type=GenericObjectPool,name=pool
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class JWTApplication {

    public static void main(String[] args) {
        SpringApplication.run(JWTApplication.class, args);

    }
}
3、全局配置

(1)webMVC

复制代码
package com.demo.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {


    //设置跨域
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("http://localhost:9528");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

(2)security

复制代码
package com.demo.security.config;

import org.springframework.security.crypto.password.PasswordEncoder;

public class MyPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(charSequence.toString());
    }

    /*public DefaultPasswordEncoder() {
        this(-1);
    }
    *//**
     * @param strength
     *            the log rounds to use, between 4 and 31
     *//*
    public DefaultPasswordEncoder(int strength) {
    }
    public String encode(CharSequence rawPassword) {
        return MD5.encrypt(rawPassword.toString());
    }
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
    }*/
}

package com.demo.security.config;

import com.demo.security.filter.LoginFilter;
import com.demo.security.filter.TokenAuthenticationFilter;
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.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityWebConfig {

    @Autowired
    private UserDetailsService userDetailsService;


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new MyPasswordEncoder();
    }


    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain configure(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {

        http.csrf(AbstractHttpConfigurer::disable);
       http.headers(AbstractHttpConfigurer::disable);

       http.sessionManagement(sessionManagement -> {
           sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
       });
       http.authorizeRequests().anyRequest().authenticated().and()

               //1、登陆、退出url,均由前端拦截器控制,这里注释掉。
               //1.1、前端拦截器中判断缓存token为空,为空则post请求访问/login,目的是进入LoginFilter获取token
               //1.2、不为空则带token访问接口,如果AuthenticationFilter拦截token不合法则根据错误码跳转到登陆页面,重复1.1的操作
               //.logout().logoutUrl("/logout").and()
               //2、身份认证filter,访问系统(除了白名单接口)需要先登陆。post请求/login接口会进入这个拦截器
               // 校验用户名密码是否正确,正确返回token给前端,不正确则返回异常信息
               .addFilterBefore(new LoginFilter(authenticationManager), LoginFilter.class)
               //3、授权filer,authenticationManager为BasicAuthenticationFilter的必传参数。所有的接口都会走到这里
               // 根据用户id查询权限,连同身份一起塞入SecurityContextHolder全局变量,后面获取用户信息则直接从SecurityContextHolder中get
               .addFilterBefore(new TokenAuthenticationFilter(authenticationManager,userDetailsService),TokenAuthenticationFilter.class);
       return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers("/param/**", "/user-websocket-endpoint/**","/menu-websocket-endpoint/**");
    }
}
4、security数据源

(1)常量(模拟数据库)

复制代码
package com.demo.security.constant;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 模拟数据库查询数据,假设有:用户名/密码/角色/资源
 * admin/123/xtgly/user_manage、role_manage、menu_manage、school_manage
 * zs/123/userAdmin、roleAdmin/user_manage、role_manage、menu_manage
 * ls/123/schoolAdmin/school_manage
 */
public class UserDBConstants {

    public static Map<String, String> getUsers() {
        Map<String,String> users = new HashMap<>();
        users.put("admin","123");
        users.put("zs","123");
        users.put("ls","123");
        return users;
    }

    public static Map<String,List<String>> getUserRoles() {
        Map<String,List<String>> userRoles = new HashMap<>();
        //admin
        List<String> adminRoles = new ArrayList<>();
        adminRoles.add("xtgly");
        userRoles.put("admin",adminRoles);
        //zs
        List<String> zsRoles = new ArrayList<>();
        zsRoles.add("userAdmin");
        zsRoles.add("roleAdmin");
        userRoles.put("zs",zsRoles);
        //ls
        List<String> lsRoles = new ArrayList<>();
        lsRoles.add("schoolAdmin");
        userRoles.put("ls",lsRoles);
        return userRoles;
    }

    public static Map<String,List<String>> getUserPermissions() {
        Map<String,List<String>> userPermissions = new HashMap<>();
        List<String> lsPermissions = new ArrayList<>();
        //ls
        lsPermissions.add("school_manage");
        userPermissions.put("ls",lsPermissions);
        //zs
        List<String> zsPermissions = new ArrayList<>();
        zsPermissions.add("user_manage");
        zsPermissions.add("role_manage");
        zsPermissions.add("menu_manage");
        userPermissions.put("zs",zsPermissions);
        //admin
        List<String> adminPermissions = new ArrayList<>();
        adminPermissions.add("user_manage");
        adminPermissions.add("role_manage");
        adminPermissions.add("menu_manage");
        adminPermissions.add("school_manage");
        userPermissions.put("admin",adminPermissions);
        return userPermissions;
    }
}

(2)UserDetails

复制代码
package com.demo.security.dto;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
@Data
public class UserDTO implements UserDetails {

    private Integer id;
    private String userName;
    private String userAccount;
    private List<String> roles;
    private List<String> menus;
    private String passWord;

    public UserDTO (Integer id,String userName,String userAccount,List<String> roles,List<String> menus,String passWord){
        this.id = id;
        this.userAccount = userAccount;
        this.userName = userName;
        this.roles = roles;
        this.menus = menus;
        this.passWord = passWord;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    public List<String> getMenus() {
        return menus;
    }

    public void setMenus(List<String> menus) {
        this.menus = menus;
    }

    @Override
    public String getPassword() {
        return passWord;
    }

    @Override
    public String getUsername() {
        return this.userAccount;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

(3)UserDetailService

复制代码
package com.demo.security.service;


import com.demo.security.constant.UserDBConstants;
import com.demo.security.dto.UserDTO;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.Map;

/**
 * 当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。
 * 而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑,
 */
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //模拟数据库查询
        Map<String, String> userMap = UserDBConstants.getUsers();
        String dbPwd = userMap.get(username);
        if(StringUtils.isEmpty(dbPwd)){
            throw new UsernameNotFoundException("用户不存在");
        }
        Map<String, List<String>> userRoles = UserDBConstants.getUserRoles();
        List<String> roles = userRoles.get(username);
        Map<String, List<String>> userMenus = UserDBConstants.getUserPermissions();
        List<String> menus = userMenus.get(username);
        UserDTO userDTO = new UserDTO(null,null,username,roles,menus,dbPwd);
        return userDTO;
    }
}
5、filter

(1)前端登录

复制代码
package com.demo.security.filter;

import com.demo.security.constant.UserDBConstants;
import com.demo.security.dto.ResponseMsg;
import com.demo.security.dto.UserDTO;
import com.demo.security.util.JwtUtil;
import com.google.gson.Gson;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.entity.ContentType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;

@Slf4j
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public LoginFilter(AuthenticationManager authenticationManager) {
        //super(new AntPathRequestMatcher("/login", "POST"));
        this.authenticationManager = authenticationManager;
    }

    /**
     * /login POST接口验证
     * @param req
     * @param res
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {
        try {
            logger.info("进入LoginFilter");
            String userName = req.getParameter("userName");
            String passWord = req.getParameter("passWord");
            if (StringUtils.isEmpty(userName)) {
                throw new UsernameNotFoundException("请输入账号");
            }
            if (StringUtils.isEmpty(passWord)) {
                throw new UsernameNotFoundException("请输入密码");
            }
            //验证用户名密码是否正确
            Map<String, String> userMap = UserDBConstants.getUsers();
            if(!userMap.keySet().contains(userName)){
                throw new UsernameNotFoundException("用户不存在");
            }
            if(!passWord.equals(userMap.get(userName))){
                throw new UsernameNotFoundException("密码错误");
            }
            //这里权限返回空,由后面的授权过滤器查询
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(userName, passWord, new ArrayList<>()));
        } catch (UsernameNotFoundException e) {
            //返回错误信息
            res.setCharacterEncoding("UTF-8");
            res.setContentType("application/text;charset=utf-8");
            try {
                res.getWriter().write(e.getMessage());
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            return null;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }


    @Override
    protected void successfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse res,
            jakarta.servlet.FilterChain chain,
            Authentication authResult)
            throws IOException{
        UserDTO userDTO = (UserDTO) authResult.getPrincipal();
        String jwtToken = JwtUtil.generateWebToken(userDTO);
        //返
        ResponseMsg resMsg = ResponseMsg.builder().code(200).data(jwtToken).build();
        res.setContentType(ContentType.TEXT_HTML.toString());
        Gson gson = new Gson();
        res.getWriter().write(gson.toJson(resMsg));
    }
}

(2)token验证

复制代码
package com.demo.security.filter;

import com.demo.security.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

import java.io.IOException;

@Slf4j
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {

    private UserDetailsService userDetailsService;

    public TokenAuthenticationFilter(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) {
        super(authenticationManager);
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
        logger.info("登陆成功后访问,url={}"+req.getRequestURI());
        String token = req.getHeader("token");
        res.setCharacterEncoding("UTF-8");
        res.setContentType("application/text;charset=utf-8");
        //1、必填token
        if(StringUtils.isEmpty(token)){
            logger.info("登陆成功后访问,url={},token为空"+req.getRequestURI());
            res.getWriter().write("token为空");
            return;
        }
        //2、校验token是否合法,合法则解析出userName
        //token可能是前端的,也可能是open api的
        String userName = JwtUtil.getUserNameByToken(req,token);
        if(StringUtils.isEmpty(userName)){
            logger.info("登陆成功后访问,url={},token错误或失效"+req.getRequestURI());
            res.getWriter().write("token错误或者失效");
            return;
        }
        //3、根据userName获取user实体,存入全局
        UserDetails currentUser = userDetailsService.loadUserByUsername(userName);
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(currentUser,null,currentUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(req, res);
    }
}
6、一些dto和常量
复制代码
package com.demo.security.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class ResponseMsg {
    private Object data;
    private Integer code;
}

package com.demo.security.constant;

import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.util.List;

public class UrlConstants {

    public static final String WEB_API_PRE = "/v1/**";
    public static final String OPEN_API_PRE = "/openApi/**";

    public static final List<AntPathRequestMatcher> WEB_MATCHERS =  List.of(
            new AntPathRequestMatcher(WEB_API_PRE));

    public static final List<AntPathRequestMatcher> OPEN_API_MATCHERS =
            List.of(
                    new AntPathRequestMatcher(OPEN_API_PRE));
}
7、JWTUtil
复制代码
package com.demo.security.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.demo.security.constant.UrlConstants;
import com.demo.security.dto.UserDTO;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Slf4j
public class JwtUtil {

    //签发方
    private static final String APPLICATION_ISSUER = "jwt_demo";
    //签发给
    private static final String TO_WEB = "to_web";
    private static final String TO_OPEN_API = "to_open_api";
    //密钥
    private static final String WEB_SECRET = "web-secret";
    private static final String OPEN_API_SECRET = "open-api-secret";
    //jwt过期事件
    public static final int EXPIRE_TIME = 30 * 60 * 1000;


    /**
     * 生成给前端的签名
     * @return 加密的token
     */
    public static String generateWebToken(UserDTO userDTO) {
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        Algorithm algorithm = Algorithm.HMAC256(WEB_SECRET);
        // 附带username信息
        return JWT.create()
                //iss:签发方
                .withIssuer(APPLICATION_ISSUER)
                //aud:接收jwt的一方
                .withAudience(TO_WEB)
                //exp:jwt的过期时间,这个过期时间必须要大于签发时间
                //.withExpiresAt()
                //其他自定义通信信息
                .withClaim("userName", userDTO.getUsername())
                //.withClaim("age",userDTO.getAge());
                .withExpiresAt(date)
                .sign(algorithm);
    }

    /**
     * open api的签名生成
     * 此处是给单元测试用的,调用方项目应该自己生成
     * @return 加密的token
     */
    public static String generateOpenApiToken(String userName) {
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        Algorithm algorithm = Algorithm.HMAC256(OPEN_API_SECRET);
        // 附带username信息
        return JWT.create()
                //iss:签发方
                .withIssuer(APPLICATION_ISSUER)
                //aud:接收jwt的一方
                .withAudience(TO_OPEN_API)
                //exp:jwt的过期时间,这个过期时间必须要大于签发时间
                //.withExpiresAt()
                //其他自定义通信信息
                .withClaim("userName", userName)
                //.withClaim("age",age);
                .withExpiresAt(date)
                .sign(algorithm);
    }

    public static String getUserNameByToken(HttpServletRequest req, String token) {
        DecodedJWT jwt = JWT.decode(token);
        //1、是否过期
        Date expireDate = jwt.getExpiresAt();
        if(expireDate.before(new Date())){
            log.error("token已过期");
            return null;
        }
        //2、签发方是否正确
        if(!APPLICATION_ISSUER.equals(jwt.getIssuer())){
            log.error("不是本项目签发的token");
            return null;
        }
        //3、是否合法
        String audience = jwt.getAudience().get(0);
        boolean check = preCheckToken(audience,token,req);
        if(!check){
            return null;
        }
        Map<String, Claim> claims = jwt.getClaims();
        String userName = claims.get("userName").asString();
        return userName;
    }

    /**
     * 根据url来判断是web还是open api更准确,
     * 如果只根据audience来判断,则web的token也可以访问open api;
     *
     * @param audience
     * @param token
     * @param req
     * @return
     */
    private static boolean preCheckToken(String audience, String token, HttpServletRequest req) {
        if(TO_WEB.equals(audience) && urlMatches(req, UrlConstants.WEB_MATCHERS)){
            log.info("这是前端token");
            return verifyToken(WEB_SECRET,token);
        }else if(TO_OPEN_API.equals(audience) && urlMatches(req,UrlConstants.OPEN_API_MATCHERS)){
            log.info("这是open api token");
            return verifyToken(OPEN_API_SECRET,token);
        }
        log.error("token来源不合法");
        return false;
    }


    public static boolean urlMatches(HttpServletRequest request, List<AntPathRequestMatcher> matchers) {
        Optional<AntPathRequestMatcher> first = matchers.stream().filter(m -> m.matches(request)).findFirst();
        return first.isPresent();
    }

    private static boolean verifyToken(String secret, String token) {
        Algorithm algorithm = Algorithm.HMAC256(secret);
        try{
            JWTVerifier verifier = JWT.require(algorithm).build();
            verifier.verify(token);
            return true;
        }catch (Exception e){
            log.error("token非法");
            return false;
        }
    }
}
8、checkUtil
复制代码
package com.demo.security.check;

import com.demo.security.dto.UserDTO;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.awt.*;
import java.util.List;

@Component("menuAuthorizationCheck")
public class MenuAuthorizationCheck {

    public boolean hasMenuAuthorization(String menuCode) {
        UserDTO currentUser = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        List<String> menus = currentUser.getMenus();
        return menus.contains(menuCode);
    }

   /* *//**
     * open api是否有权限访问
     * @param menuCode
     * @return
     *//*
    public boolean hasOpenMenuAuthorization(String menuCode) {
        UserDTO currentUser = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        List<String> menus = currentUser.getMenus();
        return menus.contains(menuCode);
    }*/
}
9、controller

(1)前端业务接口

复制代码
package com.demo.security.controller;

import com.demo.security.dto.ResponseMsg;
import com.demo.security.dto.UserDTO;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RequestMapping("/v1/user")
@RestController
public class UserController {

    @RequestMapping("/test")
    public String test(){
        return "这是user test";
    }

    @RequestMapping("/getCurrentUser")
    public ResponseMsg getCurrentUser() {
        UserDTO currentUser = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        System.out.println("当前用户为:"+currentUser);
        return ResponseMsg.builder().code(200).data(currentUser).build();
    }
}


package com.demo.security.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/v1/menu")
@RestController
public class MenuManageController {

    @PreAuthorize("@menuAuthorizationCheck.hasMenuAuthorization('menu_manage')")
    @RequestMapping("/test")
    public String test(){
        return "这是菜单管理";
    }
}

package com.demo.security.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/v1/school")
@RestController
@Slf4j
public class SchoolManageController {

    @RequestMapping("/test")
    public String test(){
        log.info("这是学校管理controller");
        return "这是学校管理";
    }
}

package com.demo.security.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/v1/role")
@RestController
public class RoleManageController {
    @RequestMapping("/test")
    public String test(){
        return "这是角色管理";
    }
}

(2)open api业务接口

复制代码
package com.demo.security.openapi;


import com.demo.security.dto.ResponseMsg;
import com.demo.security.dto.UserDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RequestMapping("/openApi/user")
@RestController
public class OpenUserController {

    @RequestMapping("/test")
    public String test(){
        return "这是open api user test";
    }

    @RequestMapping("/getCurrentUser")
    public ResponseMsg getCurrentUser() {
        UserDTO currentUser = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        System.out.println("当前用户为:"+currentUser);
        return ResponseMsg.builder().code(200).data(currentUser).build();
    }
}


package com.demo.security.openapi;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/openApi/menu")
@RestController
public class OpenMenuController {

    @PreAuthorize("@menuAuthorizationCheck.hasMenuAuthorization('menu_manage')")
    @RequestMapping("/test")
    public String test(){
        return "这是open api菜单管理";
    }
}

package com.demo.security.openapi;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/openApi/school")
@RestController
@Slf4j
public class OpenSchoolController {

    @RequestMapping("/test")
    public String test(){
        log.info("这是学校管理controller");
        return "这是open api学校管理";
    }
}
测试验证
(1)模拟前端调用

① 登录,访问 localhost:6666/jwtDemo/login?userName=admin&passWord=123

使用返回的token调用业务接口:

② 访问 localhost:6666/jwtDemo/v1/user/test

③ 访问localhost:6666/jwtDemo/v1/user/getCurrentUser

④ 访问localhost:6666/jwtDemo/v1/menu/test

⑤ 访问 localhost:6666/jwtDemo/v1/school/test

(2)模拟openapi调用

① 使用单元测试生成一个token

复制代码
package com.demo.jwt.openapi;

import com.demo.security.JWTApplication;
import com.demo.security.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest(classes = {JWTApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)
@Slf4j
public class MyTest {

    /**
     * 模拟open api调用方生成token。
     * open api应该自己生成token,双方约定好生成的算法、密钥和payload通信字段;
     * 不能调用本项目(被调用方)接口生成,否则对于生成token的接口需要加白名单,会造成接口攻击和token泄露的安全问题
     *
     */
    @Test
    public void  createOpenApiToken(){
        String userName = "zs";
        String token = JwtUtil.generateOpenApiToken(userName);
        log.info("生成token:{}",token);
    }

}

使用这个token调用业务接口:

② 访问 localhost:6666/jwtDemo/openApi/user/getCurrentUser

③ 访问 localhost:6666/jwtDemo/openApi/school/test

④ 访问 localhost:6666/jwtDemo/openApi/menu/test

(3)前端使用openapi的token调用接口

都报错

(4)openapi使用前端的token调用接口

同样都报错

相关推荐
失业写写八股文1 小时前
Spring基础:Spring的事物哪些情况下会失效
java·后端·spring
吧啦吧啦吡叭卜4 小时前
【打卡d5】快速排序 归并排序
java·算法·排序算法
大得3694 小时前
宝塔docker切换存储目录
java·docker·eureka
东阳马生架构5 小时前
Netty基础—4.NIO的使用简介一
java·网络·netty
luckyext5 小时前
Postman用JSON格式数据发送POST请求及注意事项
java·前端·后端·测试工具·c#·json·postman
程序视点5 小时前
Redis集群机制及一个Redis架构演进实例
java·redis·后端
鱼樱前端5 小时前
Navicat17基础使用
java·后端
黑风风6 小时前
深入理解Spring Boot Starter及如何自定义Starter
java·spring boot·后端
px52133446 小时前
Solder leakage problems and improvement strategies in electronics manufacturing
java·前端·数据库·pcb工艺
鱼樱前端6 小时前
Mac M1安装MySQL步骤
java·后端