Spring Security+JWT (5)
JWT Token
JWT字符串,字符之间通过"."分隔符分为三个子串。 每一个子串表示了一个功能块,总共有以下三个部分:JWT 头、有效载荷和签名。
头部:JWT 头部分是一个描述 JWT 元数据的 JSON 对象,主要设置一些规范信息,签名部分的编码格式就在头部中声明。
载荷:token中存放有效信息的部分,是 JWT 的主体内容部分,是一个 JSON 对象,包含需要传递的数据。比如用户名,用户角色,过期时间等,但是不要放密码,会泄露。
指定七个默认字段供选择。
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID 用于标识该 JWT
1. 基本流程
认证:通过自定义一个用户名和密码登录的过滤器JwtTokenLoginFilter,登录成功后生成JWT并返回。客户端调用接口需要传JWT Token,通过过滤器AuthJwtTokenFilter对请求拦截并验证Token的合法性,已经是否过期。
授权:通过使用注解(类似@PreAuthorize)或者配置(.antMatchers("/hellouser").hasAuthority("query"))对接口配置权限。authenticationEntryPoint是配置认证异常,accessDeniedHandler是配置权限异常。
2.JWT工具类
@Component
@Data
public class JwtUtils {
//秘钥
private String secret = "mysecret";
// 过期时间 毫秒
private Long expiration = 120l * 1000;
public String createToken(String userName) {
return Jwts.builder()
.setSubject(userName)
//生成时间
.setIssuedAt(new Date())
//过期时间
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String generateToken(Map<String, Object> claims, Long expiration) {
Date expirationDate = new Date(System.currentTimeMillis() + expiration);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 从token中解析出数据
*
* @param token 令牌
* @return
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
throw e;
}
return claims;
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
throw e;
}
return username;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return true;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put(Claims.ISSUED_AT, new Date());
refreshedToken = generateToken(claims, 2 * expiration);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 验证令牌
*
* @param token 令牌
* @param userInputName 用户
* @return 是否有效
*/
public Boolean validateToken(String token, String userInputName) {
String username = getUsernameFromToken(token);
return (username.equals(userInputName) && !isTokenExpired(token));
}
}
2.登录过滤器
public class JwtTokenLoginFilter extends AbstractAuthenticationProcessingFilter {
public JwtTokenLoginFilter(String filterProcessesUrl) {
super(new AntPathRequestMatcher(filterProcessesUrl, HttpMethod.POST.name()));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//获取表单提交数据
String username = request.getParameter("username");
String password = request.getParameter("password");
//封装到token中提交
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username,password);
return getAuthenticationManager().authenticate(authRequest);
}
}
3.认证过滤器
public class AuthJwtTokenFilter extends OncePerRequestFilter {
@Autowired
JwtUtils jwtUtils;
@Autowired
MyUserDetailsService myUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
/**
* token存在则校验token
* 1. token是否存在
* 2. token存在:
* 2.1 校验token中的用户名是否失效
*/
String username = "";
if (!StringUtils.isEmpty(token)) {
try {
username = jwtUtils.getUsernameFromToken(token);
//SecurityContextHolder.getContext().getAuthentication()==null 未认证则为true
if (!StringUtils.isEmpty(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = myUserDetailsService.loadUserByUsername(username);
//如果token有效
if (jwtUtils.validateToken(token, userDetails.getUsername())) {
// 将用户信息存入 authentication,方便后续校验
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 将 authentication 存入 ThreadLocal,方便后续获取用户信息
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (ExpiredJwtException e) {
response.setStatus(HttpStatus.FORBIDDEN.value());
ResponseUtils.result(response, "token已过期,重新登录!");
return;
}
}
//继续执行下一个过滤器
chain.doFilter(request, response);
}
}
4.实现UserDetailsService
@Slf4j
public class MyUserDetailsService implements UserDetailsService {
Map<String, String> user = new HashMap();
Map<String, List<String>> userRole = new HashMap<>();
{
user.put("admin", "111111");
user.put("zs", "111111");
user.put("noaccess", "111111");
//hasRole 的处理逻辑和 hasAuthority 似乎是一样的,只是hasRole 这
// 里会自动给传入的字符串前缀(默认是ROLE_ ),
// 使用 hasAuthority 更具有一致性,不用考虑要不要加 ROLE_ 前缀,
// 在UserDetailsService类的loadUserByUsername中查询权限,也不需要手动增加。
// 在SecurityExpressionRoot 类中hasAuthority 和 hasRole
// 最终都是调用了 hasAnyAuthorityName 方法。
userRole.put("admin", Arrays.asList("ROLE_admin"));
userRole.put("zs", Arrays.asList("query"));
}
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("MyUserDetailsService.loadUserByUsername 开始");
if (!user.containsKey(username)) {
throw new UsernameNotFoundException("username is not exists");
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
if (userRole.containsKey(username)) {
authorities = userRole.get(username).stream().map(row -> new SimpleGrantedAuthority(row))
.collect(Collectors.toList());
}
return new User(username, passwordEncoder.encode(user.get(username)), authorities);
}
}
5.登录成功和失败处理的Handler
@Component
public class LoginAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if (exception instanceof BadCredentialsException) {
ResponseUtils.result(response, "用户名或密码不正确!");
}
ResponseUtils.result(response, "登录失败");
}
}
@Component
public class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JwtUtils jwtTokenUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
SecurityContextHolder.getContext().setAuthentication(authentication);
//生成令牌
String accessToken = jwtTokenUtil.createToken(userDetails.getUsername());
//生成刷新令牌,如果accessToken令牌失效,则使用refreshToken重新获取令牌(refreshToken过期时间必须大于accessToken)
String refreshToken = jwtTokenUtil.refreshToken(accessToken);
Map<String, Object> resultData = new HashMap<>();
Map<String, Object> tokenData = new HashMap<>();
tokenData.put("accessToken", accessToken);
tokenData.put("refreshToken", refreshToken);
resultData.put("msg", "登录成功");
resultData.put("token", tokenData);
ResponseUtils.result(response, resultData);
}
}
6.Security配置类
@Configuration
public class JwtTokenLoginConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
MyUserDetailsService myUserDetailsService;
@Autowired
LoginAuthenticationSuccessHandler loginAuthenticationSuccessHandler;
@Autowired
LoginAuthenticationFailureHandler loginAuthenticationFailureHandler;
@Autowired
PasswordEncoder passwordEncoder;
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
JwtTokenLoginFilter filter = new JwtTokenLoginFilter("/mylogin");
filter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class));
//认证成功处理器
filter.setAuthenticationSuccessHandler(loginAuthenticationSuccessHandler);
//认证失败处理器
filter.setAuthenticationFailureHandler(loginAuthenticationFailureHandler);
httpSecurity.authenticationProvider(daoAuthenticationProvider());
//将过滤器添加到UsernamePasswordAuthenticationFilter之前执行
httpSecurity.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
DaoAuthenticationProvider daoAuthenticationProvider() {
//使用DaoAuthenticationProvider
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
//设置userDetailService
provider.setUserDetailsService(myUserDetailsService);
//设置加密算法
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
}
@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtTokenLoginConfig jwtTokenLoginConfig;
@Autowired
AuthJwtTokenFilter authJwtTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf
http.csrf().disable()
//禁用表单登录
.formLogin().disable()
//应用登录过滤器的配置,配置分离
.apply(jwtTokenLoginConfig);
http.authorizeRequests()
.antMatchers("/index", "/refreshToken")
.permitAll()
.anyRequest().authenticated()
.and()
//禁用session,JWT校验不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//处理异常情况:认证和权限
http.exceptionHandling()
//认证未通过,不允许访问异常处理器--认证异常
.authenticationEntryPoint(MyWebSecurityConfig::MyAuthenticationEntryPoint)
//认证通过,但是没权限处理器--授权异常
.accessDeniedHandler(MyWebSecurityConfig::MyAccessDeniedHandler);
http.addFilterBefore(authJwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
//
public static void MyAuthenticationEntryPoint(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.setStatus(HttpStatus.FORBIDDEN.value());
ResponseUtils.result(response, "无权限访问,请先登录!");
}
public static void MyAccessDeniedHandler(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
ResponseUtils.result(response, "无权限!");
}
}
7.测试Controller
· index接口可以命令访问 · loginstatus接口需要登录成功后才能访问,因为未设置权限,所以不需要授权就能访问。 · hello****的接口登录认证成功后,并且需要想要的权限才能访问。
授权的注解,一定要开启@EnableGlobalMethodSecurity(prePostEnabled = true)
@RestController
public class HelloController {
@GetMapping("/index")
public String index() {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("<div><a href='/hello'>hello</a></div>");
stringBuffer.append("<div><a href='/helloadmin'>helloadmin</a></div>");
stringBuffer.append("<div><a href='/hellouser'>hellouser</a></div>");
stringBuffer.append("<div><a href='/loginstatus'>loginalert</a></div>");
return stringBuffer.toString();
}
/**
* 不需要权限,只需要认证即可访问
* @return
*/
@GetMapping("/loginstatus")
public String loginalert() {
return "已经登录成功了";
}
@GetMapping("/hello")
@PreAuthorize("hasRole('admin') OR hasAuthority('query')")
public String hello() {
return "HelloController hello";
}
@GetMapping("/helloadmin")
@PreAuthorize("hasRole('admin')")
public String helloAdmin() {
return "HelloController helloAdmin";
}
@GetMapping("/hellouser")
@PreAuthorize("hasRole('admin') OR hasAuthority('query')")
public String helloUser() {
return "HelloController helloUser";
}
}
@RestController
public class LoginController {
@Autowired
JwtUtils jwtUtils;
/**
* 刷新令牌
*
* @return
*/
@PostMapping("/refreshToken")
public ResponseEntity<Object> refreshToken(HttpServletRequest request) {
//从请求头中获取refreshToken
String oldRefreshToken = request.getHeader("Authorization");
//校验refreshToken,如果令牌没有过期
if (jwtUtils.isTokenExpired(oldRefreshToken)) {
return new ResponseEntity<>("刷新令牌已过期,请重新登录!", HttpStatus.ACCEPTED);
}
//解析refreshToken
String username = jwtUtils.getUsernameFromToken(oldRefreshToken);
//生成新的accessToken
String newAccessToken = jwtUtils.createToken(username);
String newRefreshToken = jwtUtils.refreshToken(newAccessToken);
Map<String, Object> resultData = new HashMap<>();
Map<String, Object> tokenData = new HashMap<>();
tokenData.put("accessToken", newAccessToken);
tokenData.put("refreshToken", newRefreshToken);
resultData.put("msg", "刷新令牌成功");
resultData.put("token", tokenData);
return ResponseEntity.ok(resultData);
}
}
8.其他
public class ResponseUtils {
public static void result(HttpServletResponse response, Object msg) throws IOException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream out = response.getOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
out.write(objectMapper.writeValueAsString(msg).getBytes("UTF-8"));
out.flush();
out.close();
}
}
@SpringBootApplication
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityJwtProgram {
public static void main(String[] args) {
SpringApplication.run(SecurityJwtProgram.class, args);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public MyUserDetailsService myUserDetailsService() {
return new MyUserDetailsService();
}
// 自定义的Jwt Token校验过滤器
@Bean
public AuthJwtTokenFilter authJwtTokenFilter() {
return new AuthJwtTokenFilter();
}
}
9.测试
在MyUserDetailsService中分别创建了三个账号,admin,zs, noaccess,所以对这三个账号分别验证。
mylogin接口:
loginstatus接口:
hello接口:
helloadmin接口:
hellouser接口:
参考:
SpringBoot+SpringSecurity+JWT实现认证和授权_springboot security jwt-CSDN博客