Spring Security的项目中集成JWT Token令牌安全访问后台API

您所在的位置:网站首页 spring-security-jwt Spring Security的项目中集成JWT Token令牌安全访问后台API

Spring Security的项目中集成JWT Token令牌安全访问后台API

#Spring Security的项目中集成JWT Token令牌安全访问后台API| 来源: 网络整理| 查看: 265

引言

最近接了一个私活项目,后台使用的是Spring Boot脚手架搭建的,认证和鉴权框架用的Spring Security。同时为了确保客户端安全访问后台服务的API,需要用户登录成功之后返回一个包含登录用户信息的jwt token, 用于调用其他接口时将此jwt token携带在请求头中作为调用者的认证信息。最近一个多月一方面在忙着做这个项目,另一方面恰好遇上了精彩的世界杯,也没怎么发文了。很多时候真的深感写篇原创文章比单纯的敲代码麻烦多了,但是好久不更文还是要检讨一下自己的惰性,客服自身的惰性是每个想要突破自我、不甘平庸的普通人的一辈子都不能松懈的重任。

JWT简介

首先,让我们来补一下jwt的知识。jwt token 的全称叫JSON Web Token ,主要用于在各方之间以JSON 对象方式安全地传输信息。此信息是数字签名的,可以验证和信任,JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。

虽然 JWT 可以加密以在各方之间提供保密性,但我们将专注于签名令牌。签名的令牌可以验证其中包含的声明的完整性,而加密的令牌会向其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,只有持有私钥的一方才可以签署。

jwt token 的适用场景鉴权(Authorization):这是最常见的场景。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且能够在不同的域中轻松使用。信息交换(Information Exchange):JWT令牌是在各方之间安全传输信息的好方法。因为可以对 JWT 进行签名(例如,使用公钥/私钥对),所以可以确定发件人就是他们所说的那个人。此外,由于使用 header和payload 计算签名,还可以验证内容是否被篡改。jwt 的结构

JWT 由header、payload和signature三部分组成,以 . 分割:

header:通常由令牌的类型(即 JWT)以及正在使用的签名算法(例如 HMAC SHA256 或 RSA)两部分组成;

{ "alg": "HS256", "typ": "JWT" }

然后,这个 JSON 被 Base64Url 编码以形成 JWT 的第一部分。

payload: 有效负载。其中包含声明,声明是关于实体(通常是用户)和附加数据的陈述。声明分为三种类型:registered, public, private claims.

注册(registered)声明:这是一组预定义的声明,不是强制性的,但建议使用。以提供一组有用的、可互操作的声明。比如:issue(发行人)、expired(到期时间)、subject(主题)、aud(受众)等。

公共(public)声明:这些可以由使用人随意定义。但是为了避免冲突,应该在jwt token 注册中定义,或者定义为包含抗冲突命名空间的 URI。

私有(private)声明:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,既不是注册声明也不是公共声明。

示例如下:

{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }

然后对有效负载进行 Base64Url 编码以形成 JSON Web 令牌的第二部分

注意,对于已签名的令牌,此信息虽然受到保护以防篡改,但任何人都可以读取。除非已加密,否则请勿将机密信息放入 JWT 的有效负载或标头元素中。

Signature: 要创建签名部分,必须获取已编码的标头(header)、编码的有效负载(payload)、密钥、header中指定的算法,并对其进行签名。

例如,如果您想使用 HMAC SHA256 算法,签名将通过以下方式创建:

HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

签名用于验证信息在传输过程中是否被篡改,并且在使用私钥签名令牌的情况下,它还可以验证 JWT 的发送者是否正确。

完整jwt

由三个 . 分隔的 Base64-URL 字符串,可以在 HTML 和 HTTP 环境中轻松传递,相对于基于 XML 的标准(如 SAML)则更紧凑。

下面显示了一个 JWT,该 JWT 具有前面介绍过的header和payload编码,并使用密钥签名:

我们可以在 https://links.jianshu.com/go?to=https%3A%2F%2Fjwt.io%2F%23debugger-io网站来解码、验证和生成 JWT。

jwt 的使用方式

在身份校验中,当用户成功登录,将返回一个 JSON Web Token。由于令牌是凭据,因此必须非常小心以防止出现安全问题。

通常令牌需要设置一个过期时间,超过过期时间则令牌失效,需要置换新的令牌。

由于缺乏安全性,不应该将敏感的会话数据存储在浏览器中。每当用户需要访问受保护的路由或资源时,用户代理应该发送jwt,通常在 Authorization header 中使用 Bearer 模式。header 的内容应如下所示:

Authorization: Bearer

某些情况下,这可以是一种无状态授权机制。服务器的受保护路由将检查 Authorization header 中是否存在有效的 JWT,如果存在,则允许用户访问受保护的资源。如果 JWT 包含必要的数据,则可能会减少查询数据库以进行某些操作的需要,尽管情况并非总是如此。

如果 token 在 Authorization header,跨域 Cross-Origin Resource Sharing (CORS)不是问题,因为它不使用 cookies。

客户端获取jwt令牌访问受保护资源的具体流程

1) 用户在在客户端使用用户名/密码登录;

2)服务端使用密钥生成一个JWT令牌;

3)服务端将生存的jwt令牌返回给浏览器;

4)用户拿到jwt 令牌放到Authentication参数对应的请求头中访问服务端受保护的资源和API;

5)服务端校验签名,从jwt令牌中解析获取用户信息;

6)服务端校验签名通过并从jwt令牌中解析出用户信息,则返回API的成功响应信息给客户端

Spring Security 安全框架下使用jwt token

在非spring security框架下的spring boot项目中使用jwt令牌鉴权,我们只需要新建一个拦截器或者Servlet过滤器解析jwt token信息就行了,解析成功就放行请求,解析失败则返回403权限不足信息就行了。但是在Spring Security 框架中本身就自动适配了很多个过滤器,并组成了一个过滤器链,因此我们也需要新建一个解析jwt token的过滤器加入过滤器链中才行。

新建一个spring boot项目

使用IDEA新建spring boot项目的同时添加一些必要的依赖jar包,如spring mvc、mysql驱动、druid数据源和fast-json及代码简洁工具lombok等

4.0.0 org.springframework.boot spring-boot-starter-parent 2.2.7.RELEASE com.bonus bonus-backend 1.0.0-SNAPSHOT bonus-backend bonus-backend 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-configuration-processor true com.alibaba druid-spring-boot-starter 1.2.8 com.alibaba fastjson 1.2.79 mysql mysql-connector-java 8.0.16 runtime org.projectlombok lombok true org.springframework.boot spring-boot-maven-plugin 加入spring security 和 jwt 相关依赖项

在项目的pom.xml文件的dependencies标签中加入

commons-codec commons-codec 1.11 com.baomidou mybatis-plus-boot-starter 3.1.2 org.springframework.boot spring-boot-starter-security 2.2.7.RELEASE com.auth0 java-jwt 3.7.0 项目配置文件

application-porperties

server.servlet.context-path=/bonus spring.profiles.active=dev spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8 # mybatis-plus config mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml mybatis-plus.configuration.map-underscore-to-camel-case=true mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl # twelve.zodiac twelve.zodiac.mouse=03,15,27,39 twelve.zodiac.cow=02,14,26,38 twelve.zodiac.tiger=01,13,25,37,49 twelve.zodiac.rabbit=12,24,36,48 twelve.zodiac.dragon=11,23,35,47 twelve.zodiac.snake=10,22,34,46 twelve.zodiac.horse=09,21,33,45 twelve.zodiac.sheep=08,20,32,44 twelve.zodiac.monkey=07,19,31,43 twelve.zodiac.chicken=06,18,30,42 twelve.zodiac.dog=05,17,29,41 twelve.zodiac.pig=04,16,28,40

application-dev.properties

server.address=127.0.0.1 server.port=8090 spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.druid.url=jdbc:mysql://localhost:3306/bonus?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai spring.datasource.druid.username=bonus_user spring.datasource.druid.password=tiger2022@ spring.datasource.druid.validation-query=select 1 from dual #spring.datasource.druid.connect-properties # redis config spring.redis.client-name=redis-client spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.database=0

日志打印配置

log4j.properties

log4j.rootLogger=DEBUG,stdout log4j.logger.com.baomidou.mybatisplus=DEBUG log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%5p %d %C: %m%n 启动类@SpringBootApplication @EnableConfigurationProperties @EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled=true, jsr250Enabled=true) public class BonusBackendApplication { public static void main(String[] args) { SpringApplication.run(BonusBackendApplication.class, args); } }

启动类中除了加上@SpringBootApplication注解之外,还加上了开启配置属性生效的注解@EnableConfigurationProperties以及全局安全访问注解@EnableGlobalMethodSecurity进行动态权限校验

JWT相关API

用于生成jwt token 和从 jwt token中解析出用户信息的相关API都在com.auth0.jwt.JWT和com.auth0.jwt.JWTCreator两个类中。

JWT 类中的API方法

public JWT(): JWT类实例的构造方法;public static Builder create(): 创建jwt token的构建器, 返回对象为JWTCreator类中的静态内部类Builderpublic DecodedJWT decodeJwt(String token): 解析jwt token方法public static DecodedJWT decode(String token) : 静态解析jwt token方法public static Verification require(Algorithm algorithm): 通过算法构造Verification对象静态方法, Verification类主要用来校验jwt令牌是否有效

JWTCreator类中的API方法

静态内部类Builder主要用于构造header和payload中 的内容, 该静态类主要提供一些列withXXX方法用于指定相应的键值对内容,主要有一下API方法:

public JWTCreator.Builder withHeader(Map headerClaims): 构造header代表的键值对集合;public JWTCreator.Builder withKeyId(String keyId): 指定令牌header中的kid的值;public JWTCreator.Builder withIssuer(String issuer): 指定令牌发行者;public JWTCreator.Builder withSubject(String subject): 指定令牌主题;public JWTCreator.Builder withAudience(String... audience): 指定令牌受众,通过该方法可以将令牌授予有限数量的用户;public JWTCreator.Builder withExpiresAt(Date expiresAt): 指定令牌过期日期;public JWTCreator.Builder withNotBefore(Date notBefore): 指定令牌不能早于某个日期使用;public JWTCreator.Builder withIssuedAt(Date issuedAt): 指定令牌签发日期;public JWTCreator.Builder withJWTId(String jwtId): 指定令牌id;public JWTCreator.Builder withClaim(String name, Boolean value): 指定payload中的键值对,值为布尔类型;public JWTCreator.Builder withClaim(String name, Integer value): 指定payload中的键值对,值为Integer类型;public JWTCreator.Builder withClaim(String name, Long value) : 指定payload中的键值对,值为Long类型;public JWTCreator.Builder withClaim(String name, Double value): 指定payload中的键值对,值为Double类型;public JWTCreator.Builder withClaim(String name, String value): 指定payload中的键值对,值为String类型;public JWTCreator.Builder withClaim(String name, Date value): 指定payload中的键值对,值为Date类型;public JWTCreator.Builder withArrayClaim(String name, String[] items): 指定payload中的键值对,值为String数组类型;public JWTCreator.Builder withArrayClaim(String name, Integer[] items): 指定payload中的键值对,值为Integer数组类型;public JWTCreator.Builder withArrayClaim(String name, Long[] items): 指定payload中的键值对,值为Long数组类型;public String sign(Algorithm algorithm) : 签名方法,通过算法签名,得到完整的jwt token内容方法

algorithm算法对象可通过静态方法Algorithem#HMAC256或者Algorithem#HMAC512方法创建,入参为一个String类型的密钥

JWTDecoder类中的API方法

JWTDecoder类为DecodedJWT类的实现类,主要用来从解析jwt令牌后的对象中获取想要的字段信息

public String getAlgorithm(): 获取签名算法名称;public String getType(): 获取jwt令牌的类型,默认为jwt;public String getKeyId(): 获取jwt 令牌header中的kid对应的值;public Claim getHeaderClaim(String name): 获取header中指定名字的Claim, 它可以进一步把value代表的数据转成各种数据类型;public String getIssuer(): 获取jwt令牌的签发人;public String getSubject():获取jwt令牌的主题;public List getAudience(): 获取jwt 令牌的受众;public Date getExpiresAt(): 获取jwt令牌过期时间;public Date getNotBefore(): 获取令牌不能早于使用的时间;public String getId(): 获取令牌id;public Claim getClaim(String name): 获取指定名字Claim;public Map getClaims(): 获取jwt令牌中的Claim键值对集合;public String getHeader(): 获取jwt令牌中的header部分内容;public String getPayload(): 获取jwt令牌中的payload部分内容;public String getSignature(): 获取jwt 令牌中签名部分内容;public String getToken(): 还原jwt令牌内容;新建Jwt令牌工具类

利用JWT相关API我们新建了一个JwtTokenUtil的工具类用于生成jwt令牌

public class JwtTokenUtil { // 密钥 private static final String SECRET = "bonusBACKEND2022$"; // 过期时间7天 private static final int EXPIRE_SECONDS = 7*24*3600; private final static Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class); /** * 生成token方法 * @param memInfoMap * @return jwtToken */ public static String genAuthenticatedToken(Map memInfoMap){ List authorities = (List) memInfoMap.get("authorities"); String authorityStr = null; if(authorities!=null && authorities.size()>0){ StringBuffer buffer = new StringBuffer(); for(int i=0; i0){ for(RoleInfoDTO roleInfoDTO: roleInfoDTOList){ SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + roleInfoDTO.getRoleName().toUpperCase()); memInfoDTO.getAuthorities().add(grantedAuthority); } } return memInfoDTO; }

MemInfoDTO类源码如下:

@Data @TableName("bonus_mem_info") @ApiModel(value="MemInfoDTO", description = "会员DTO") @Validated public class MemInfoDTO extends BaseDTO implements UserDetails { /** * 会员id */ @TableId @ApiModelProperty(name = "memId", value = "memId", notes = "会员ID", dataType = "Long") private Long memId; /** * 会员账号 */ @TableField(value = "mem_account") @NotEmpty(message = "会员账号不能为空") @ApiModelProperty(name="memAccount", value = "memAccount", notes = "会员账号", dataType = "String") private String memAccount; /** * 会员密码 */ @TableField(value = "mem_pwd") @NotEmpty(message = "会员密码不能为空") @ApiModelProperty(name="memPwd", value = "memPwd", notes = "加密后的会员密码", dataType = "String") private String memPwd; /** * 会员类型:1-vip;2-代理 */ @TableField(value = "mem_type") @NotEmpty(message = "会员类型不能为空") @ApiModelProperty(name="memType", value = "memType", notes = "会员类型", dataType = "Integer", example = "1", allowableValues = "1,2") private Integer memType; /** * 会员信用额度,单位分 */ @TableField(value = "total_credit_amount") @NotEmpty(message = "会员信用额度不能为空") @ApiModelProperty(name = "totalCreditAmount", value = "totalCreditAmount", notes = "会员总信用额度,单位分", dataType = "Long", example = "10000") private Long totalCreditAmount; /** * 会员已使用信用额度,单位分 */ @ApiModelProperty(name = "usedCreditAmount", value = "usedCreditAmount", notes = "会员已使用信用额度,单位分", dataType = "Long", example = "5000") @TableField(value = "used_credit_amount") private Long usedCreditAmount; @TableField(exist = false) private List authorities = new ArrayList(); @Override public Collection getAuthorities() { return authorities; } @Override public String getPassword() { return this.memPwd; } @Override public String getUsername() { return this.memAccount; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } } 新建JwtToken认证过滤器public class JwtAuthenticationFilterBean extends GenericFilterBean { private final static Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilterBean.class); private String AUTHORIZATION_NAME = "Authorization"; private String BEARER = "Bearer"; // 放行白名单 private static List whiteRequestList = new ArrayList(); // whiteRequestList中加入无需认证和鉴权的请求url static { whiteRequestList.add("/bonus/member/checkSafetyCode"); whiteRequestList.add("/bonus/login"); whiteRequestList.add("/bonus/member/login"); whiteRequestList.add("/bonus/common/kaptcha"); whiteRequestList.add("/bonus/admin/login"); whiteRequestList.add("/bonus/favicon.ico"); whiteRequestList.add("/bonus/doc.html"); whiteRequestList.add("/bonus/error"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; logger.info("requestUrl="+request.getRequestURI()); //白名单请求和静态资源直接放行 if(whiteRequestList.contains(request.getRequestURI()) || (request.getRequestURI().contains("admin/dist") && request.getRequestURI().endsWith(".css") || request.getRequestURI().equals(".js") || request.getRequestURI().endsWith(".png") || request.getRequestURI().endsWith("favicon.ico"))){ // 如果是登录和安全码验证请求直接放行 filterChain.doFilter(servletRequest, servletResponse); return; } else { String bearerToken = request.getHeader(AUTHORIZATION_NAME); if(StringUtils.isEmpty(bearerToken)||!bearerToken.startsWith(BEARER)){ printException(response, HttpStatus.UNAUTHORIZED.value(), "缺失jwt令牌或令牌格式错误"); return; } String authToken = bearerToken.substring(bearerToken.indexOf(BEARER)+BEARER.length()+1); if(StringUtils.isEmpty(authToken)){ String message = "http header Authorization is null, user Unauthorized"; response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpStatus.UNAUTHORIZED.value()); this.printException(response, HttpStatus.UNAUTHORIZED.value(), message); return; } else { try { // 解析jwt令牌, 若是Bearer模式则需要先判断是否前缀为Bearer,然后再截取 // Bearer空格后面的内容再进行解析 DecodedJWT decodedJWT = JWT.decode(authToken); Map claimMap = decodedJWT.getClaims(); Claim expireClaim = claimMap.get("exp"); Date expireDate = expireClaim.asDate(); // 校验token 是否过期 if(expireDate.before(DateUtil.date(System.currentTimeMillis()))){ String message = "Authorization token expired"; this.printException(response, HttpStatus.UNAUTHORIZED.value(), message); return; } Claim memAccountClaim = claimMap.get("memAccount"); if(memAccountClaim==null || StringUtils.isEmpty(memAccountClaim.asString())){ String message = "memAccount cannot be null"; response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpStatus.UNAUTHORIZED.value()); this.printException(response, HttpStatus.UNAUTHORIZED.value(), message); return; } // 请求头认证通过, 放行请求 filterChain.doFilter(servletRequest, servletResponse); } catch (JWTDecodeException e) { String message = "JWT decode authToken failed, caused by " + e.getMessage(); this.printException(response, HttpStatus.UNAUTHORIZED.value(), message); return; } } } } /** * 打印请求头认证失败信息 * @param response * @param status * @param message * @throws IOException */ private void printException(HttpServletResponse response, int status, String message) throws IOException { logger.error(message); response.setStatus(status); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); PrintWriter printWriter = response.getWriter(); ResponseResult responseResult = ResponseResult.error(status, message); printWriter.write(JSONObject.toJSONString(responseResult)); printWriter.flush(); printWriter.close(); } } Spring Security配置类中配置登录成功后返回jwt令牌@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { private final static Logger logger = LoggerFactory.getLogger(SecurityConfig.class); @Resource private MemInfoService memInfoService; private MathContext mathContext = new MathContext(2, RoundingMode.HALF_UP); @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { super.configure(auth); auth.userDetailsService(memInfoService); } @Override public void configure(WebSecurity web) { web.ignoring().antMatchers("/static/**","/index.html","/templates/**", "/admin/**", "/doc.html", "/webjars/**", "/v2/*", "/favicon.ico", "/swagger-resources"); } @Override protected void configure(HttpSecurity http) throws Exception { JwtAuthenticationFilterBean jwtAuthenticationFilterBean = new JwtAuthenticationFilterBean(); http.addFilterBefore(jwtAuthenticationFilterBean, UsernamePasswordAuthenticationFilter.class); // 将JwtToken认证过滤器注册在登录认证过滤器之前 // 配置跨域 http.cors().configurationSource(corsConfigurationSource()) .and().logout().invalidateHttpSession(true).logoutUrl("/member/logout").permitAll() ; http.authorizeRequests().antMatchers("/member/checkSafetyCode").permitAll() .antMatchers("/doc.html").permitAll() .antMatchers("/common/kaptcha").permitAll() .antMatchers("/admin/login").permitAll() .anyRequest().authenticated() .and().httpBasic() .and().formLogin() .loginProcessingUrl("/member/login") // 登录接口 .successHandler((httpServletRequest, httpServletResponse, authentication) -> { httpServletResponse.setContentType("application/json;"); httpServletResponse.setStatus(HttpStatus.OK.value()); PrintWriter printWriter = httpServletResponse.getWriter(); MemInfoDTO memInfoDTO = (MemInfoDTO) authentication.getPrincipal(); Map userMap = new HashMap(); userMap.put("memId", memInfoDTO.getMemId()); userMap.put("memAccount", memInfoDTO.getMemAccount()); userMap.put("memPwd", memInfoDTO.getMemPwd()); BigDecimal totalCredit = memInfoDTO.getTotalCreditAmount()!=null?new BigDecimal(memInfoDTO.getTotalCreditAmount()/100, mathContext): new BigDecimal("0.0"); userMap.put("totalCreditAmount", totalCredit); BigDecimal usedCredit = memInfoDTO.getUsedCreditAmount()!=null?new BigDecimal(memInfoDTO.getUsedCreditAmount()/100, mathContext):new BigDecimal("0.0"); userMap.put("usedCreditAmount", usedCredit); Long remainCredit = (memInfoDTO.getTotalCreditAmount()==null?0:memInfoDTO.getTotalCreditAmount()) - (memInfoDTO.getUsedCreditAmount()==null?0:memInfoDTO.getUsedCreditAmount()); BigDecimal remainCreditAmount = new BigDecimal(remainCredit/100, mathContext); userMap.put("remainCreditAmount", remainCreditAmount); userMap.put("authorities", memInfoDTO.getAuthorities()); Map dataMap = new HashMap(); dataMap.put("memInfo", userMap); dataMap.put("authenticatedToken", JwtTokenUtil.genAuthenticatedToken(userMap)); ResponseResult responseResult = ResponseResult.success(dataMap, "login success"); printWriter.write(JSONObject.toJSONString(responseResult)); printWriter.flush(); printWriter.close(); }).failureHandler((httpServletRequest, httpServletResponse, e) -> { logger.error("login failed, caused by " + e.getMessage()); httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); httpServletResponse.setStatus(HttpStatus.OK.value()); PrintWriter printWriter = httpServletResponse.getWriter(); ResponseResult responseResult = ResponseResult.error(HttpStatus.UNAUTHORIZED.value(), "authentication failed"); responseResult.setPath(httpServletRequest.getRequestURI()); printWriter.write(JSONObject.toJSONString(responseResult)); printWriter.flush(); printWriter.close(); }).permitAll() .and().csrf().disable().exceptionHandling().accessDeniedHandler(accessDeniedHandler()); } //配置跨域访问资源 private CorsConfigurationSource corsConfigurationSource() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.addAllowedOrigin("*"); //同源配置,*表示任何请求都视为同源,若需指定ip和端口可以改为如“localhost:8080”,多个以“,”分隔; corsConfiguration.addAllowedHeader("*");//header,允许哪些header,本案中使用的是token,此处可将*替换为token; corsConfiguration.addAllowedMethod("*"); //允许的请求方法,PSOT、GET等 corsConfiguration.setAllowCredentials(true); // 注册跨域配置 source.registerCorsConfiguration("/**",corsConfiguration); //配置允许跨域访问的url return source; } @Bean AccessDeniedHandler accessDeniedHandler() { return new AuthenticationAccessDeniedHandler(); } } 测试效果

在启动类中运行Main方法运行服务后就可以测试效果了

测试生成jwt令牌

我们首先测试生成jwt token的登录接口, 在postman中调用登录接口

post http://localhost:8090/bonus/member/login??username=zhangsan&password=zhangsan1234

接口返回信息如下:

{ "code": 200, "data": { "memInfo": { "memAccount": "zhangsan", "totalCreditAmount": 2000, "memPwd": "82dea760d7bb362ca74883836ee4d6ba", "remainCreditAmount": 2000, "usedCreditAmount": 0, "authorities": [ { "authority": "ROLE_USER" } ], "memId": 1592927262097924097 }, "authenticatedToken": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZW1BY2NvdW50IjoiemhhbmdzYW4iLCJ0b3RhbENyZWRpdEFtb3VudCI6MjAwMC4wLCJtZW1Qd2QiOiI4MmRlYTc2MGQ3YmIzNjJjYTc0ODgzODM2ZWU0ZDZiYSIsInJlbWFpbkNyZWRpdEFtb3VudCI6MjAwMC4wLCJ1c2VkQ3JlZGl0QW1vdW50IjowLjAsImV4cCI6MTY3MjU1ODAyMSwiaWF0IjoxNjcxOTUzMjIxLCJqdGkiOiI2M2M1YmExZDIzZGY0YjIzODQ1NWU5YjkwNzQzMzRmMSIsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJtZW1JZCI6MTU5MjkyNzI2MjA5NzkyNDA5N30.S5UQLasL-SALKBHwhhUk_DGv__YPlRJQ7TC1pBzxb0g" }, "message": "login success" }

memPwd字段为密码加密后的密文

authenticatedToken 对应的内容为Bearer模式的jwt令牌, 真正的jwt令牌内容为eyj开头的那串较长的字符串。

测试通过jwt令牌认证与鉴权

新建一个获取配置数据的接口

@RestController @RequestMapping("/config") public class ConfigController { @Resource private ZodiacProperties zodiacProperties; @GetMapping("/twelve/zodiacs") public ResponseResult getTwelveZodiacs(){ return ResponseResult.success(zodiacProperties); } }

ZodiacProperties类源码如下:

@Component @ConfigurationProperties(prefix = "twelve.zodiac") public class ZodiacProperties { private String mouse; private String cow; private String tiger; private String rabbit; private String dragon; private String snake; private String horse; private String sheep; private String monkey; private String chicken; private String dog; private String pig; // 省略set、get方法 }

接口写好后,重启后台服务,并重新登录拿到jwt令牌令牌

首先试一下不在请求头中加入jwt令牌的结果

GET http://localhost:8090/bonus/config/twelve/zodiacs

接口返回结果:

{ "code": 401, "message": "缺失jwt令牌或令牌格式错误" }

然后在请求头中加入Authentication参数jwt令牌再次测试结果:

此时返回结果:

{ "code": 200, "message": "ok", "path": null, "data": { "mouse": "03,15,27,39", "cow": "02,14,26,38", "tiger": "01,13,25,37,49", "rabbit": "12,24,36,48", "dragon": "11,23,35,47", "snake": "10,22,34,46", "horse": "09,21,33,45", "sheep": "08,20,32,44", "monkey": "07,19,31,43", "chicken": "06,18,30,42", "dog": "05,17,29,41", "pig": "04,16,28,40" } }

关于如何在集成spring security安全访问框架的spring boot项目中如何使用jwt令牌安全访问服务端API就讲到这里

参考阅读

【1】JWT token 介绍(https://www.jianshu.com/p/fa957f32806a)



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3