前言
最近有个想法想整理一个内容比较完整springboot项目初始化Demo。
SpringBoot接口权限控制 SpringSecurity 接口使用 Bearer token类型 JWT 鉴权
一、pom文件新增依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.12.6</version>
</dependency>
application.yaml 增加参数 用于模拟后续生成TOKEN和验证使用
#JTW 加密的密码
jwt:# secret 必须足够长 40bits之上# 否则 报错 The specified key byte array is 40 bits which is not secure enough #for any JWT HMAC-SHA algorithmsecret: 12345678123456781234567812345678
#认证系统编号 clientid: clientid
二、创建 UserInfo 类 模拟用户信息
import lombok.Data;/*** 用户信息*/
@Data
public class UserInfo {private String clientid;//用户授权idprivate String username;//用户名private String password;//用户名
}
三、创建 JwtTokenUtil
类,用于生成和验证JWT令牌。
package com.murg.bootdemo.util;import com.murg.bootdemo.business.entity.UserInfo;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecureDigestAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import javax.crypto.SecretKey;
import java.io.Serializable;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;@Component
public class JwtTokenUtil implements Serializable {/*** 过期时间(单位:秒)*/public static final Integer REFRESH_TOKEN_EXPIRE = 72000;/*** 加密算法*/private final static SecureDigestAlgorithm<SecretKey, SecretKey> ALGORITHM = Jwts.SIG.HS256;/*** 私钥*/private static String SECRET;/*** 秘钥实例*/public static SecretKey KEY;/*** jwt签发者*/private final static String JWT_ISS = "Sunreal";/*** jwt主体*/private final static String SUBJECT = "dwd";@Value("${jwt.secret}")public void setSecret(String secret) {JwtTokenUtil.SECRET = secret;JwtTokenUtil.KEY=Keys.hmacShaKeyFor(SECRET.getBytes());}/*** 获取令牌用户数据* @param token* @return*/public static UserInfo getUserInfoByToken(String token) {final Claims claims = parsePayload(token);UserInfo userInfo = null;if (null != claims) {userInfo = new UserInfo();userInfo.setClientid(claims.get("clientid").toString());userInfo.setUsername(claims.get("username").toString());}return userInfo;}/*** 获取令牌用户数据* @param token* @return*/public static String getUsernameFromToken(String token) {final Claims claims = parsePayload(token);UserInfo userInfo = null;if (null != claims) {userInfo = new UserInfo();userInfo.setClientid(claims.get("clientid").toString());userInfo.setUsername(claims.get("username").toString());}return userInfo.getUsername();}/*** 令牌过期校验 true-过期 false-未过期* @param token* @return*/public static Boolean isExpired(String token) {Boolean result = true;final Claims claims = parsePayload(token);if (null != claims) {String exp = claims.get("exp").toString();long diff = Long.parseLong(exp) - System.currentTimeMillis() / 1000;if(diff > 0) {result = false;}}return result;}public Boolean validateToken(String token, UserDetails userDetails) {final String username = getUsernameFromToken(token);return (username.equals(userDetails.getUsername()) && !isExpired(token));}/*** 获取令牌的过期时间*/public static Long getTokenExpireIn(String token) {Long expireIn = System.currentTimeMillis() / 1000;final Claims claims = parsePayload(token);if (null != claims) {String exp = claims.get("exp").toString();return Long.parseLong(exp);}return expireIn;}public String generateToken(UserDetails userDetails,String clientid) {Map<String, Object> claims = new HashMap<>();claims.put("username",userDetails.getUsername());// claims.put("password",userDetails.getPassword());claims.put("clientid",clientid);return generateToken(claims);}/*** 生成令牌*/public static String generateToken(Map<String, Object> payload) {Date expireDate = Date.from(Instant.now().plusSeconds(REFRESH_TOKEN_EXPIRE));return Jwts.builder().header().add("typ", "JWT").add("alg", "HS256").and().claims(payload).id(UUID.randomUUID().toString()).expiration(expireDate).issuedAt(new Date()).subject(SUBJECT).issuer(JWT_ISS).signWith(KEY, ALGORITHM).compact();}/*** 解析令牌claims*/public static Jws<Claims> parseClaim(String token) {Jws<Claims> claimsJws = null;claimsJws = Jwts.parser() .verifyWith(KEY) .build().parseSignedClaims(token);return claimsJws;}/*** 解析令牌header*/private static JwsHeader parseHeader(String token) {Jws<Claims> claimsJws = parseClaim(token);if(null == claimsJws) {return null;}return claimsJws.getHeader();}/*** 解析令牌payload*/private static Claims parsePayload(String token) {Jws<Claims> claimsJws = parseClaim(token);if(null == claimsJws) {return null;}return claimsJws.getPayload();}
}
四、创建 JwtAuthenticationEntryPoint
类,用于处理未经授权的请求
package com.murg.bootdemo.filter;import com.murg.bootdemo.common.WebResult;
import com.murg.bootdemo.util.JsonUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;/**此类继承Spring Security的AuthenticationEntryPoint类,并重写其commence。它拒绝每个未经身份验证的请求并发送错误代码401。*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {private static final long serialVersionUID = -7858869558953243875L;@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response,AuthenticationException authException) throws IOException, ServletException {//返回统一封装的格式WebResult<Object> webResult = WebResult.ok();webResult.isWrong("您可能未登录或登录超时,请重新登陆!");response.setContentType("application/json");response.setStatus(401);try {JsonUtil.write2Stream(response.getOutputStream(), webResult);} catch (Exception var6) {throw new ServletException();}}
}
五、创建JWTUserDetailsService以便使用用户名从数据库中获取用户详细信息。
package com.murg.bootdemo.interceptor;import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;import java.util.ArrayList;/*** JWTUserDetailsService* JWTUserDetailsService实现了Spring Security UserDetailsService接口。* 它会覆盖loadUserByUsername,以便使用用户名从数据库中获取用户详细信息。* 当对用户提供的用户详细信息进行身份验证时,Spring Security Authentication Manager调用此方法从数据库中获取用户详细信息* 在这里,我们从硬编码的用户列表中获取用户详细信息。* 以后实际会从数据库中获取用户详细信息的DAO实现。* 用户密码也使用BCrypt以加密格式存储。在这里,您可以使用在线Bcrypt生成器为密码生成Bcrypt。*/
@Service
public class JwtUserDetailsService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {if ("admin".equals(username)) {return new User("admin", "$2a$10$XRemBdMT1hrf4EdfsDWHCeSQFczKq0STVDqVRXT8YnbPoQcN21KAi",new ArrayList<>());} else {throw new UsernameNotFoundException("User not found with username: " + username);}}/*** 测试生成BCrypt加密格式* @param args*/public static void main(String[] args) {String pass = "123456";BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();final String passHash = encoder.encode(pass);System.out.println(passHash);final boolean matches = encoder.matches(pass, passHash);System.out.println(matches);}
}
六、创建 JwtRequestFilter
类,用于解析和验证JWT
package com.murg.bootdemo.filter;import com.murg.bootdemo.interceptor.JwtUserDetailsService;
import com.murg.bootdemo.util.JwtTokenUtil;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/**JwtRequestFilter继承了Spring Web的OncePerRequestFilter类。对于任何传入请求,都会执行此Filter类。它检查请求是否具有有效的JWT令牌。如果它具有有效的JWT令牌,则它将在上下文中设置Authentication,以指定当前用户已通过身份验证。*/
@Slf4j
@Component
public class JwtRequestFilter extends OncePerRequestFilter {private final JwtUserDetailsService jwtUserDetailsService;private final JwtTokenUtil jwtTokenUtil;public JwtRequestFilter(JwtUserDetailsService jwtUserDetailsService, JwtTokenUtil jwtTokenUtil) {this.jwtTokenUtil = jwtTokenUtil;this.jwtUserDetailsService = jwtUserDetailsService;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException {final String requestTokenHeader = request.getHeader("Authorization");String username = null;String jwtToken = null;// JWT Token is in the form "Bearer token". Remove Bearer word and get only the Tokenif (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {jwtToken = requestTokenHeader.substring(7);try {username = jwtTokenUtil.getUserInfoByToken(jwtToken).getUsername();} catch (IllegalArgumentException e) {log.error("Unable to get JWT Token");} catch (ExpiredJwtException e) {log.error("JWT Token has expired"); //token 过期抛出异常 用于统一异常处理ExceptionConfig捕获 throw e;}} else {logger.warn("JWT Token does not begin with Bearer String");}//Once we get the token validate it.if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {//根据用户名获取库里的用户信息UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);// if token is valid configure Spring Security to manually set authenticationif (jwtTokenUtil.validateToken(jwtToken, userDetails)) { //校验用户信息 用户名一致并且token没过期/* 创建了一个UsernamePasswordAuthenticationToken实例,并通过调用setDetails方法设置了额外的细节。这个细节可以是任何对象,通常用于传递额外的信息,如客户端IP地址、会话ID等。请注意,setDetails方法不是为了设置用户名和密码,而是为了设置与认证过程相关的其他信息。如果你需要设置用户名和密码,应该在创建UsernamePasswordAuthenticationToken实例时提供。*/UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));// 设置SecurityContext中的Authentication对象 在上下文中设置Authentication,以指定当前用户已通过身份验证SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);}}chain.doFilter(request, response);}
}
七、增加配置Spring Security类 WebSecurityConfig
package com.murg.bootdemo.config;import com.murg.bootdemo.filter.JwtAuthenticationEntryPoint;
import com.murg.bootdemo.filter.JwtRequestFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;private final UserDetailsService jwtUserDetailsService;private final JwtRequestFilter jwtRequestFilter;public WebSecurityConfig(JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,UserDetailsService jwtUserDetailsService,JwtRequestFilter jwtRequestFilter) {this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;this.jwtUserDetailsService = jwtUserDetailsService;this.jwtRequestFilter = jwtRequestFilter;}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/***认证管理器的建造器,如果要使用该功能需要配置一个userDetailsService和PasswordEncoder;为什么要配置UserDetailService,他的作用是什么?因为我们需要被认证的用户作为管理,那么UserDetailService作用就是:在认证器中根据传过来的用户名进行查找,PasswordEncoder就是将密码进行比对;认证成功后返回一个Authentication对象自定义用户认证的service 用于实现自己的认证逻辑passwordEncoder定义密码解密方式**/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception{auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());}@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception {// We don't need CSRF for this examplehttpSecurity.csrf().disable()// 不验证此特定请求 该请求为获取token的接口.authorizeRequests().antMatchers("/authenticate").permitAll()// 所有其他请求都需要经过身份验证.anyRequest().authenticated().and()//Spring Security中用于配置认证入口点异常处理的一部分。当Spring Security遇到认证异常时,// 它会通过配置的AuthenticationEntryPoint来处理。.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()// 所有的Rest服务一定要设置为无状态,以提升操作性能.sessionCreationPolicy(SessionCreationPolicy.STATELESS);// Add a filter to validate the tokens with every requesthttpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);}}
八、测试接口鉴权
重启服务不带token测试helloWord接口,返回鉴权失败
创建GetJwtController
调用上述预留放开的/authenticate接口获取token数据
package com.murg.bootdemo.business.controller;import com.murg.bootdemo.common.WebResult;
import com.murg.bootdemo.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.*;import java.util.Objects;/**使用JwtAuthenticationController暴露/authenticate。使用Spring Authentication Manager验证用户名和密码。如果凭据有效,则会使用JWTTokenUtil创建一个JWT令牌并将其提供给客户端。*/
@RestController
public class GetJwtController {@Value("${jwt.clientid}")private String CLIENTID;private final AuthenticationManager authenticationManager;private final JwtTokenUtil jwtTokenUtil;private final UserDetailsService jwtInMemoryUserDetailsService;public GetJwtController(AuthenticationManager authenticationManager,JwtTokenUtil jwtTokenUtil,UserDetailsService jwtInMemoryUserDetailsService) {this.authenticationManager = authenticationManager;this.jwtTokenUtil = jwtTokenUtil;this.jwtInMemoryUserDetailsService = jwtInMemoryUserDetailsService;}@RequestMapping(value = "/authenticate", method = RequestMethod.POST)public WebResult createAuthenticationToken(@RequestBody JwtRequest authenticationRequest)throws Exception {if(!CLIENTID.equals(authenticationRequest.getClientid())) {throw new Exception( "客户端未授权");}// authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());final UserDetails userDetails = jwtInMemoryUserDetailsService.loadUserByUsername(authenticationRequest.getUsername());final String token = jwtTokenUtil.generateToken(userDetails,authenticationRequest.getClientid());return WebResult.ok(token);}/*** 这个方法会调用UserDetailsServiceImpl.loadUserByUsername,此过程中调用的流程如下:* 1. AuthenticationManager是个接口,ProviderManager是他的实现类。* 2. authenticationManager.authenticate()调用其实就是ProviderManager.authenticate()* 3. ProviderManager代码中找到provider.authenticate(authentication);* 4. provider.authenticate(authentication)是AuthenticationProvider.authenticate(authentication)方法。* 5. AuthenticationProvider也是一个接口,他的实现类是AbstractUserDetailsAuthenticationProvider。* 6. AbstractUserDetailsAuthenticationProvider的子类是DaoAuthenticationProvider。* 7. 在DaoAuthenticationProvider中调用了UserDetailsService8.写一个UserDetailsServiceImpl实现类JwtUserDetailsService ,实现UserDetailsService ,并重写loadUserByUsername()直接调用UserDetails userDetails = jwtInMemoryUserDetailsService.loadUserByUsername(authenticationRequest.getUsername());*/private void authenticate(String username, String password) throws Exception {Objects.requireNonNull(username);Objects.requireNonNull(password);try {authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));} catch (DisabledException e) {throw new Exception("USER_DISABLED", e);} catch (BadCredentialsException e) {throw new Exception("INVALID_CREDENTIALS", e);}}
}
拿到token之后 重新带 Bearer 类型 token 测试 helloWord接口 正常返回
九、配置鉴权白名单
通过application.yaml配置放行鉴权路由白名单
1.增加配置 放行getTt26接口。
oauth:whitelist:- /getTt26
2.创建配置类 AuthwhitelistConfig 定义属性 whitelist
参数配置在 application.yaml 文件中,通过 @ConfigurationProperties
注解注入参数whitelist
package com.murg.bootdemo.config;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;import java.util.List;
@Data
@Configuration
@ConfigurationProperties("oauth")
public class AuthwhitelistConfig {private List<String> whitelist;
}
3修改 WebSecurityConfig方法
引入AuthwhitelistConfig
private final AuthwhitelistConfig authwhitelist;
增加白名单不验证
.antMatchers(authwhitelist.getWhitelist().toArray(new String[0])).permitAll()
@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception {// We don't need CSRF for this examplehttpSecurity.csrf().disable()// 不验证此特定请求.authorizeRequests().antMatchers("/authenticate").permitAll().antMatchers(authwhitelist.getWhitelist().toArray(new String[0])).permitAll()// 所有其他请求都需要经过身份验证.anyRequest().authenticated().and()//Spring Security中用于配置认证入口点异常处理的一部分。当Spring Security遇到认证异常时,// 它会通过配置的AuthenticationEntryPoint来处理。.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()// 所有的Rest服务一定要设置为无状态,以提升操作性能.sessionCreationPolicy(SessionCreationPolicy.STATELESS);// Add a filter to validate the tokens with every requesthttpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);}
不带token测试接口