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调用接口
同样都报错