背景
前段时间做了一个项目, 因为涉及到权限认证, 所以分别调研了 SpringSecurity 和 Apache Shiro. 最后选择使用了 SpringSecurity + JWT做权限认证, 现在项目已经结束, 总相关笔记.
项目下载地址 jwt-demo
- 使用JWT生成token
- token存储在数据库中
- 使用 application/json 登录
- 使用手机号进行登录
- URI动态拦截
配置过程
添加依赖
- 分别添加 SpringSecurity JWT 和 fastjson 依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.60</version> </dependency>
|
基础准备对象
- 主要是在用户登录成功handle时使用JWT生成Token返回给客户端.
基础使用dto
请求返回基类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Data public class BaseReqDto implements Serializable {
private String version;
}
@Data public class BaseRespDto implements Serializable {
private String resultCode;
private String resultMsg;
private String resultTime;
}
|
登录请求返回对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Data public class LoginReqDto {
private String username;
private String token;
}
@Data public class LoginRespDto extends BaseRespDto {
private String token;
}
|
用于验证的用户
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| package com.liuzhihang.demo.bean;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable; import java.util.Collection;
public class UserDetailsImpl implements UserDetails, Serializable {
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; }
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) { this.authorities = authorities; }
@Override public String getPassword() { return this.password; }
@Override public String getUsername() { return this.username; }
public void setUsername(String username) { this.username = username; }
public void setPassword(String password) { this.password = password; }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; } }
|
用户未登录handle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
@Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
@Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
response.setContentType("application/json;charset=UTF-8");
LoginRespDto respDto = new LoginRespDto(); respDto.setResultCode("0001"); respDto.setResultMsg("用户未登录"); respDto.setResultTime(LocalDateTime.now().format(FORMATTER));
response.getWriter().write(JSON.toJSONString(respDto)); } }
|
用户登录验证失败handle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
@Component public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
@Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
response.setContentType("application/json;charset=UTF-8");
LoginRespDto respDto = new LoginRespDto(); respDto.setResultCode("0001"); respDto.setResultMsg("用户登录认证失败"); respDto.setResultTime(LocalDateTime.now().format(FORMATTER));
response.getWriter().write(JSON.toJSONString(respDto)); } }
|
用户无权访问handle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
@Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
@Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
LoginRespDto respDto = new LoginRespDto(); respDto.setResultCode("0002"); respDto.setResultMsg("用户无权访问"); respDto.setResultTime(LocalDateTime.now().format(FORMATTER));
response.getWriter().write(JSON.toJSONString(respDto));
} }
|
用户登录成功handle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
@Slf4j @Component public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
@Resource private JwtTokenUtil jwtTokenUtil;
@Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
String jwtToken = jwtTokenUtil.generateToken(userDetails);
response.setContentType("application/json;charset=UTF-8");
LoginRespDto respDto = new LoginRespDto(); respDto.setToken(jwtToken); respDto.setResultCode("0000"); respDto.setResultMsg("登录成功"); respDto.setResultTime(LocalDateTime.now().format(FORMATTER));
response.getWriter().write(JSON.toJSONString(respDto));
} }
|
JwtTokenUtil
主要用来生成token和通过token解析对象等操作.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
| package com.liuzhihang.demo.utils;
import com.liuzhihang.demo.bean.UserDetailsImpl; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component;
import java.time.Instant; import java.util.Date;
@Component public class JwtTokenUtil {
private static final SignatureAlgorithm SIGN_TYPE = SignatureAlgorithm.HS256;
public static final String SECRET = "jwt-secret";
public static final long EXPIRED_TIME = 7 * 24 * 60 * 60 * 1000L;
public String generateToken(UserDetails userDetails) {
long instantNow = Instant.now().toEpochMilli();
Claims claims = Jwts.claims(); claims.put(Claims.SUBJECT, userDetails.getUsername());
return Jwts.builder().setClaims(claims).setIssuedAt(new Date(instantNow)) .setExpiration(new Date(instantNow + EXPIRED_TIME)) .signWith(SIGN_TYPE, SECRET).compact(); }
public String generateToken(String userName) {
long instantNow = Instant.now().toEpochMilli();
Claims claims = Jwts.claims(); claims.put(Claims.SUBJECT, userName);
return Jwts.builder().setClaims(claims).setIssuedAt(new Date(instantNow)) .setExpiration(new Date(instantNow + EXPIRED_TIME)) .signWith(SIGN_TYPE, SECRET).compact(); }
public UserDetails getUserDetailsFromToken(String jwtToken) {
Claims claimsFromToken = getClaimsFromToken(jwtToken);
String userName = claimsFromToken.get(Claims.SUBJECT, String.class);
UserDetailsImpl userDetails = new UserDetailsImpl(); userDetails.setUsername(userName);
return userDetails; }
public Boolean validateToken(String token, UserDetails userDetails) { UserDetailsImpl user = (UserDetailsImpl) userDetails; String username = getPhoneNoFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token)); }
public String refreshToken(String token) { String refreshedToken; try { Claims claims = getClaimsFromToken(token);
long instantNow = Instant.now().toEpochMilli();
refreshedToken = Jwts.builder().setClaims(claims).setIssuedAt(new Date(instantNow)) .setExpiration(new Date(instantNow + EXPIRED_TIME)) .signWith(SIGN_TYPE, SECRET).compact(); } catch (Exception e) { refreshedToken = null; } return refreshedToken; }
public Boolean isTokenExpired(String token) { Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); }
public String getPhoneNoFromToken(String token) { return getClaimsFromToken(token).getSubject(); }
public Date getExpirationDateFromToken(String token) { return getClaimsFromToken(token).getExpiration(); }
private Claims getClaimsFromToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); }
}
|
WebSecurityConfig 核心配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| package com.liuzhihang.demo.config;
import com.liuzhihang.demo.filter.CustomizeAuthenticationFilter; import com.liuzhihang.demo.filter.JwtPerTokenFilter; import com.liuzhihang.demo.service.UserDetailServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private UserDetailServiceImpl userDetailServiceImpl;
@Resource private JwtPerTokenFilter jwtPerTokenFilter;
@Resource(name = "authenticationEntryPointImpl") private AuthenticationEntryPoint authenticationEntryPoint;
@Resource(name = "authenticationSuccessHandlerImpl") private AuthenticationSuccessHandler authenticationSuccessHandler;
@Resource(name = "authenticationFailureHandlerImpl") private AuthenticationFailureHandler authenticationFailureHandler;
@Resource(name = "accessDeniedHandlerImpl") private AccessDeniedHandler accessDeniedHandler;
@Autowired public void configureUserInfo(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailServiceImpl);
}
@Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().httpBasic().authenticationEntryPoint(authenticationEntryPoint)
.and().formLogin().permitAll()
.loginProcessingUrl("/login")
.and().authorizeRequests() .antMatchers("/login", "/getVersion") .permitAll() .anyRequest().access("@dynamicAuthorityService.hasPermission(request,authentication)")
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.and().addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtPerTokenFilter, UsernamePasswordAuthenticationFilter.class)
.headers().cacheControl();
}
@Bean public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); }
@Bean CustomizeAuthenticationFilter customAuthenticationFilter() throws Exception { CustomizeAuthenticationFilter filter = new CustomizeAuthenticationFilter(); filter.setAuthenticationSuccessHandler(authenticationSuccessHandler); filter.setAuthenticationFailureHandler(authenticationFailureHandler); filter.setAuthenticationManager(authenticationManagerBean()); return filter; }
}
|
登录校验过程
graph TD;
A(请求登录) --> B(CustomizeAuthenticationFilter#attemptAuthentication 解析请求的json);
B --> C(UserDetailServiceImpl#loadUserByUsername 验证用户名密码);
C --> D(AuthenticationSuccessHandlerImpl#onAuthenticationSuccess 构建返回参数 包括token);
D --> E(返回结果)
自定义拦截器解析 json 报文
前端请求登录报文类型为 application/json 需要后端增加拦截器, 对登录请求报文进行解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| package com.liuzhihang.demo.filter;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONException; import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException;
@Slf4j public class CustomizeAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE) || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) { try { BufferedReader br = request.getReader(); String str; StringBuilder jsonStr = new StringBuilder(); while ((str = br.readLine()) != null) { jsonStr.append(str); }
log.info("本次登录请求参数:{}", jsonStr);
JSONObject jsonObject = JSON.parseObject(jsonStr.toString());
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( jsonObject.getString("username"), jsonObject.getString("password")); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } catch (IOException e) { log.info("用户登录, 请求参数 不正确"); throw new AuthenticationServiceException("获取报文请求参数失败"); } catch (JSONException e) { log.info("用户登录, 请求报文格式 不正确"); throw new AuthenticationServiceException("请求报文, 转换Json失败"); } } else { log.error("用户登录, contentType 不正确"); throw new AuthenticationServiceException( "请求 contentType 不正确, 请使用 application/json;charset=UTF-8 或者 application/json;"); }
}
}
|
用户认证模块
- 根据获取到的username从数据库中查询到密码, 将用户名密码赋值给UserDetails对象, 返回其他的框架会进行校验
- 这边使用中是使用的手机号+验证码登录, 所以 上面json解析的也是 phoneNo+verificationCode
- 在这块 username仅仅代指登录名, 可以是手机号可以是别的.
- 这边使用中验证码是从redis中获取的. 获取不到返回失败, 获取到和传递的不一致也算失败.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package com.liuzhihang.demo.service;
import com.liuzhihang.demo.bean.UserDetailsImpl; import lombok.extern.slf4j.Slf4j; 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.Component;
@Slf4j @Component("userDetailServiceImpl") public class UserDetailServiceImpl implements UserDetailsService {
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetailsImpl userDetailsImpl = new UserDetailsImpl(); userDetailsImpl.setUsername("liuzhihang"); userDetailsImpl.setPassword(new BCryptPasswordEncoder().encode("123456789")); return userDetailsImpl; }
}
|
请求校验过程
graph TD;
A(请求接口) --> B(JwtPerTokenFilter#doFilterInternal 验证Header中的token);
B --> C(DynamicAuthorityService#hasPermission 验证有没有请求url权限);
C --> D(处理逻辑);
D --> E(返回结果)
JWTToken拦截器
主要是拦截请求, 验证Header中的token是否正确
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| package com.liuzhihang.demo.filter;
import com.liuzhihang.demo.utils.JwtTokenUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; 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;
@Slf4j @Component public class JwtPerTokenFilter extends OncePerRequestFilter {
@Autowired private JwtTokenUtil jwtTokenUtil;
private static final String HEADER_STRING = "token";
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader(HEADER_STRING); if (null != token && !jwtTokenUtil.isTokenExpired(token)) { UserDetails userDetails = jwtTokenUtil.getUserDetailsFromToken(token); String username = userDetails.getUsername();
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
if (username.equals("liuzhihang")) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } filterChain.doFilter(request, response); }
}
|
URI动态校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| package com.liuzhihang.demo.service;
import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest; import java.util.HashSet; import java.util.Set;
@Slf4j @Component(value = "dynamicAuthorityService") public class DynamicAuthorityService {
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
try { Object principal = authentication.getPrincipal(); if (principal instanceof UserDetails && authentication instanceof UsernamePasswordAuthenticationToken) { String uri = request.getRequestURI();
UserDetails userDetails = (UserDetails) principal;
String username = userDetails.getUsername(); log.info("本次用户请求认证, username:{}, uri:{}", username, uri);
if (username.equals("liuzhihang")){ Set<String> set = new HashSet<>(); set.add("/homeInfo"); set.add("/getAllUser"); set.add("/editUserInfo"); if (set.contains(uri)) { return true; } }
} } catch (Exception e) { log.error("用户请求登录, uri:{} error", request.getRequestURI(), e); return false; } return false; } }
|
测试
脚本在 httpclient脚本
1 2 3 4 5 6 7 8 9 10 11 12 13
| POST localhost:8080/login Content-Type: application/json
{ "username": "liuzhihang", "password": "123456789" } ### 请求接口脚本
POST localhost:8080/homeInfo Content-Type: application/json token: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJsaXV6aGloYW5nIiwiaWF0IjoxNTY5MDI1NjY4LCJleHAiOjE1Njk2MzA0Njh9.Kot_uLnwtcq-t5o4x3V-xBnpf-mKEi7OV2eAfgMCKLk ###
|
返回:
1 2 3 4 5 6
| { "resultCode": "0000", "resultMsg": "登录成功", "resultTime": "20190920191038", "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJsaXV6aGloYW5nIiwiaWF0IjoxNTY4OTc3ODM4LCJleHAiOjE1Njk1ODI2Mzh9.MAS9VkFdCF3agkCgTtc0VzPMFjY42vFyIvAEzkSeAfs" }
|
参考
前后端分离 SpringBoot + SpringSecurity + JWT + RBAC 实现用户无状态请求验证