简介
什么是JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。JWT官网: https://jwt.io/
JWT的构成
一个JWT由三个部分组成:header,payload,signature。分别保存了不同的信息。
头信息指定了JWT使用的签名算法
header={alg=HS512}
消息体用来承载要传递的数据,它的一个属性对被称为claim,这样的标准成为claims标准,同样是将其用Base64Url编码
部分是将前两个部分的json拼接中间加一点,再将这个拼接后的字符串用alg中的算法处理
JWT的请求流程
- 用户使用账号和面发出post请求;
- 服务器使用私钥创建一个jwt;
- 服务器返回这个jwt给浏览器;
- 浏览器将该jwt串在请求头中像服务器发送请求;
- 服务器验证该jwt;
- 返回响应的资源给浏览器。
下面开始代码讲解
首先是依赖的引入:
这里对于数据库的处理我用的是Mybatis,用Jpa的小伙伴相应的地方做一些改变即可
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.41</version>
</dependency>
<dependency>
<groupId>org.springframework.mobile</groupId>
<artifactId>spring-mobile-device</artifactId>
<version>1.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
自定义一个实现UserDetails接口的user类
@Data
public class JwtUser implements UserDetails {
private String stuId;
private String password;
public JwtUser(String stuId, String password) {
this.stuId = stuId;
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return stuId;
}
@Override
public String getUsername() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
自定义一个实现UserDetailService的实现类,实现自定义用户验证
其中对于数据库的访问每个人都不同,所以并没有给出代码,需要有一个通过用户输入的用户名来查询用户信息的方法getUserByUserId(userId).
@Service
@Slf4j
public class JwtUserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String stuId) throws UsernameNotFoundException {
User user = userService.getUserByUserId(stuId);
if(user == null) {
log.info("用户不存在");
throw new UsernameNotFoundException(String.format("用户名为 %s 的用户不存在!", stuId));
} else {
return new JwtUser(stuId, user.getPassword());
}
}
}
Token的工具类
这里可以直接复制粘贴过去
@Component
public class JwtTokenUtil {
//秘钥
private String secret = "myscret";
//过期时间
private long expiration = 1296000L;
static final String CLAIM_KEY_USERNAME = "sub";
static final String CLAIM_KEY_AUDIENCE = "audience";
static final String CLAIM_KEY_CREATED = "created";
private static final String AUDIENCE_UNKNOWN = "unknown";
private static final String AUDIENCE_WEB = "web";
private static final String AUDIENCE_MOBILE = "mobile";
private static final String AUDIENCE_TABLET = "tablet";
public String getUsernameFromToken(String token) {
String username;
try {
final Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
public Date getCreatedDateFromToken(String token) {
Date created;
try {
final Claims claims = getClaimsFromToken(token);
created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
} catch (Exception e) {
created = null;
}
return created;
}
public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
//得到token的有效期
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
public String getAudienceFromToken(String token) {
String audience;
try {
final Claims claims = getClaimsFromToken(token);
audience = (String) claims.get(CLAIM_KEY_AUDIENCE);
} catch (Exception e) {
audience = null;
}
return audience;
}
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
//设置过期时间
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
// return new Date(30 * 24 * 60);
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
private String generateAudience(Device device) {
String audience = AUDIENCE_UNKNOWN;
if (device.isNormal()) {
audience = AUDIENCE_WEB;
} else if (device.isTablet()) {
audience = AUDIENCE_TABLET;
} else if (device.isMobile()) {
audience = AUDIENCE_MOBILE;
}
return audience;
}
private Boolean ignoreTokenExpiration(String token) {
String audience = getAudienceFromToken(token);
return (AUDIENCE_TABLET.equals(audience) || AUDIENCE_MOBILE.equals(audience));
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
final Date created = getCreatedDateFromToken(token);
return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&& (!isTokenExpired(token) || ignoreTokenExpiration(token));
}
public String refreshToken(String token) {
String refreshedToken;
try {
final Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
//TODO,验证当前的token是否有效
public Boolean validateToken(String token, UserDetails userDetails) {
JwtUser user = (JwtUser) userDetails;
final String username = getUsernameFromToken(token);
final Date created = getCreatedDateFromToken(token);
return (username.equals(user.getUsername())&& !isTokenExpired(token));
}
}
自定义一个拦截器类,用于拦截未进行身份认证的情况
@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setHeader("Access_Control_Allow_Origin","*");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("text/html; charset=utf-8");
ResultVO result = ResultVOUtil.error(ResultEnum.AUTHENTICATION_ERROR);
log.info("需要身份认证:{}" ,result);
httpServletResponse.getWriter().append(JSON.toJSONString(result));
}
}
自定义JwtAuthenticationTokenFilter拦截器,用于进行自定义用户身份验证
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
private String tokenHeader = "Authorization";
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
if (httpServletRequest.getMethod().equals("OPTIONS")) {
//TODO,这里是解决前后端对接时的跨域问题
log.info("浏览器的请求预处理");
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE");
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With,Content-Type,Accept,Authorization,token,Cookie");
return;
} else {
String requestUrl = httpServletRequest.getRequestURI();
log.info("requestURL: {}", requestUrl);
String authToken = httpServletRequest.getHeader(this.tokenHeader);
String stuId = jwtTokenUtil.getUsernameFromToken(authToken);
log.info("checking authentication for user " + stuId);
//当token中的username不为空时进行验证token是否是有效的token
if (stuId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//token中username不为空,并且Context中的认证为空,进行token验证
//TODO,从数据库得到带有密码的完整user信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(stuId);
log.info("加载userDetails:{}", userDetails.getUsername());
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
/**
* UsernamePasswordAuthenticationToken继承AbstractAuthenticationToken实现Authentication
* 所以当在页面中输入用户名和密码之后首先会进入到UsernamePasswordAuthenticationToken验证(Authentication),
* 然后生成的Authentication会被交由AuthenticationManager来进行管理
* 而AuthenticationManager管理一系列的AuthenticationProvider,
* 而每一个Provider都会通UserDetailsService和UserDetail来返回一个
* 以UsernamePasswordAuthenticationToken实现的带用户名和密码以及权限的Authentication
*/
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
log.info("authenticated user " + stuId + ", setting security context");
//将authentication放入SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
}
WebSecurityConfig配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(this.userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
//token的验证方式不需要开启csrf的防护
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
//设置无状态的连接,即不创建session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 当前的url允许进行匿名访问,即不需要身份认证
.antMatchers(
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
//配置swagger界面的匿名访问
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/images/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/v2/api-docs").permitAll()
.antMatchers("/configuration/ui").permitAll()
.antMatchers("/configuration/security").permitAll()
.antMatchers("/login").permitAll()
//配置允许匿名访问的路径
.anyRequest().authenticated();
//配置自己的验证过滤器
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity.headers().cacheControl();
}
}
最后就是service的逻辑层
@Service
@Slf4j
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUserDetailServiceImpl jwtUserDetailService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
public User getUserByUserId(String userId) {
return userMapper.getUserByUserId(userId);
}
@Override
public Map login(String username, String password) {
User user = userMapper.getUserByUserId(username);
if (user == null) {
Map map = new HashMap();
map.put("code",0);
map.put("message","用户不存在.");
return map;
}
UserDetails userDetails = jwtUserDetailService.loadUserByUsername(username);
if (!(new BCryptPasswordEncoder().matches(password, userDetails.getPassword()))) {
Map map = new HashMap();
map.put("code",1);
map.put("message","密码错误..");
return map;
}
//TODO,将username和password被获得后封装到UsernamePasswordAuthenticationToken
Authentication token = new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities());
//TODO,token被传递给AuthenticationManager进行验证
Authentication authentication = authenticationManager.authenticate(token);
//将生成的authentication放入容器中,生成安全的上下文
log.info("验证成功.");
SecurityContextHolder.getContext().setAuthentication(authentication);
//生成token
final String realToken = jwtTokenUtil.generateToken(userDetails);
Map map = new HashMap();
map.put("message", "token生成成功.");
map.put("token", realToken);
return map;
}
}
Controller层写登陆接口
@RestController
public class LogInController {
@Autowired
private UserService userService;
/**
* 登录接口
*/
@PostMapping("/login")
public Map login(String username, String password) {
return userService.login(username, password);
}
}
下面失去postman测试的结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/2019081515042150.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDI5NjcwNw==,size_16,color_FFFFFF,t_70