概述
越来越多的网站开发都采用前后端分离架构。典型的像React\vue等。本篇文章会介绍下基于SpringBoot架构后端的实现。
JWT
JWT原理
JWT通讯原理
具体认证流程:
1、输入用户/密码,服务端认证成功后。会返回客户端一个JWT Token。
格式:
IUzUxMiJ9.eyJzdWIiOiJoMS.PqZg8jCah0hgj
2、客户端将token保存到本地(可以放LocalStorage,也可以放Cookie)
3、当前端访问一个受保护的路由或资源,需要在HTTP请求头的Authorization字段中使用Bearer模式添加JWT。格式如下:Authorization: Bearer
4、服务端利用Secrity过滤器,获取HTTP头里面的这个token值,解析是否为有效的Token。如果是则放行。
Spring Security框架
SpringSecurity 采用的是责任链的设计模式,是一堆过滤器链的组合,它有一条很长的过滤器链。简化一下:
登录流程过滤器链
示例代码目录:
示例工程
跟Security相关的几个文件代码如下:
WebSecurityConfig.java 配置文件
package com.aliyun.agp.webcommon.security;
import com.aliyun.agp.webcommon.security.jwt.JwtAuthenticationEntryPoint;
import com.aliyun.agp.webcommon.security.jwt.JwtRequestFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
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;
@Configurable
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private UserDetailsService jwtUserDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Value("${jwt.get.token.uri}")
private String authenticationPath;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(jwtUserDetailsService)
.passwordEncoder(passwordEncoderBean());
}
@Bean
public PasswordEncoder passwordEncoderBean() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
// 指定的接口直接放行
.antMatchers(HttpMethod.POST,"/api/login").permitAll()
.antMatchers(HttpMethod.GET,"/api/refresh").permitAll()
// 其他接口需要认证才能请求
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
// 不需要创建session,前后端分离
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
这个文件里面要注意配置哪些URI是不需要认证的。没有认证的会到哪个类处理。以及自定义过滤器。
JwtAuthenticationEntryPoint.java这个类的作用就是没有登录的处理逻辑。
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
AgpWebResult agpWebResult = AgpWebResult.buildFailWithErrCode(HttpServletResponse.SC_UNAUTHORIZED,"Unauthorized");
httpServletResponse.getWriter().write(AgpUtils.toJsonString(agpWebResult));
httpServletResponse.flushBuffer();
}
}
再有一个就是过滤器了。这块跟采用JWT还是用其他协议有关。需要拿到前端提交过来的请求信息。
JwtRequestFilter.java自定义过滤器
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(LoggerConstants.AGP_APPEND);
@Autowired
private UserDetailsService jwtUserDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
public static final String TOKEN_PREFIX = "Bearer ";
public static final String HEADER_STRING = "Authorization";
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
final String requestTokenHeader = httpServletRequest.getHeader(HEADER_STRING);
String username = null;
String jwtToken = null;
if (requestTokenHeader != null && requestTokenHeader.startsWith(TOKEN_PREFIX)) {
jwtToken = requestTokenHeader.replace(TOKEN_PREFIX,"");
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
}catch (IllegalArgumentException e) {
logger.error("JWT_TOKEN_UNABLE_TO_GET_USERNAME", e);
} catch (ExpiredJwtException e) {
logger.warn("the token is expired and not valid anymore", e);
} catch (SignatureException e) {
logger.error("Authentication Failed. Username or Password not valid.");
} catch (MalformedJwtException exception) {
logger.warn("Request to parse invalid JWT : failed : {}", exception.getMessage());
}
} else {
logger.warn("JWT_TOKEN_DOES_NOT_START_WITH_BEARER_STRING");
}
logger.debug("JWT_TOKEN_USERNAME_VALUE '{}'", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails,null,userDetails.getAuthorities()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
logger.info("authenticated user " + username + ", setting security context");
// 设置成全局ThreadLocal级别的上下文,用于获取当前登录用户
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
用户密码认证实现。Security是将用户的密码与输入的密码在内存中比较是否Match的。
JwtUserDetailsService.java提供loadUserByUsername方法
@Service
public class JwtUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userService.findByUsername(s).orElseThrow(() -> new UsernameNotFoundException("user not found with username:" + s));
User jwtUser = new User();
jwtUser.setId(user.getId());
jwtUser.setUserName(user.getUserName());
jwtUser.setPassword(passwordEncoder.encode(user.getPassword()));
jwtUser.setEmail(user.getEmail());
jwtUser.setRoles(user.getRoles());
return JwtUserDetails.build(jwtUser);
}
}
JwtUserDetails.java实现UserDetails接口
public class JwtUserDetails implements UserDetails {
private Long id;
private String username;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public JwtUserDetails(Long id, String username, String email, String password,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static JwtUserDetails build(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
return new JwtUserDetails(
new Long(user.getId()),
user.getUserName(),
user.getEmail(),
user.getPassword(),
authorities);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
JwtUserDetails user = (JwtUserDetails) o;
return Objects.equals(id, user.id);
}
@JsonIgnore
public Long getId() {
return id;
}
}
跟JWT 其实就一个工具类,用于生成token,校验token的。
import io.jsonwebtoken.Clock;
import io.jsonwebtoken.impl.DefaultClock;
import org.springframework.beans.factory.annotation.Value;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.cglib.core.internal.Function;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtTokenUtil implements Serializable {
@Value("${app.jwtSecret}")
private String secret;
@Value("${jwt.token.expiration.in.seconds}")
private Long expiration;
private Clock clock = DefaultClock.INSTANCE;
/***
* 从token中获取用户名
* @param token
* @return
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
/**
* 获取过期时间
*
* @param token
* @return
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public Boolean canTokenBeRefreshed(String token) {
return (!isTokenExpired(token));
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
/**
* 验证token是否有效
*
* @param token
* @param userDetails
* @return
*/
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
/***
* 给用户生成新的token
* @param authentication
* @return
*/
public String generateToken(Authentication authentication) {
Map<String, Object> claims = new HashMap<>();
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String username = userDetails.getUsername();
return doGenerateToken(claims, username);
}
/***
* 当token过期之后需要刷新token
* @param token
* @return
*/
public String refreshToken(String token) {
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);
final Claims claims = getAllClaimsFromToken(token);
claims.setIssuedAt(createdDate);
claims.setExpiration(expirationDate);
return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact();
}
private Date calculateExpirationDate(Date createdDate) {
return new Date(createdDate.getTime() + expiration * 1000);
}
/***
* 生成一个新token,顺序:
* 1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
* 2. Sign the JWT using the HS512 algorithm and secret key.
* 3. According to JWS Compact
* @param claims
* @param subject 以用户名为subject进行生成token
* @return
*/
private String doGenerateToken(Map<String, Object> claims, String subject) {
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);
return Jwts.builder().setClaims(claims).setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
/***
* 判断当前token是否过期
* @param token
* @return
*/
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret)
.parseClaimsJws(token).getBody();
}
}
入口控制器
@RestController
@CrossOrigin
public class JwtAuthenticationController extends BaseController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.http.request.header}")
private String tokenHeader;
@Autowired
private JwtUserDetailsService userDetailsService;
@RequestMapping(value = "/api/login", method = RequestMethod.POST)
public AgpWebResult createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) {
AgpWebResult agpWebResult = null;
try {
// 先利用Security框架校验用户名、密码是否对的
Authentication authentication = authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
// 利用JWT工具生成token,客户端需要写到浏览器
final String token = jwtTokenUtil.generateToken(authentication);
agpWebResult = AgpWebResult.buildSuccess(token);
} catch (Exception e) {
agpWebResult = AgpWebResult.buildFailWithErrCode(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
}
return agpWebResult;
}
}
Postman效果
非登录态,访问需要授权API:
{
"data": null,
"errorCode": 401,
"errorMsg": "Unauthorized",
"statusCode": "401",
"success": false
}
登录接口,返回值:
{
"data": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIifQ.gztc6-yNTmKiQ---bIsVoSa",
"success": true,
"errorMsg": null,
"statusCode": null,
"errorCode": null
}
如果输入的用户名或密码不对,返回:
{
"data": null,
"success": false,
"errorMsg": "INVALID_CREDENTIALS",
"statusCode": "401",
"errorCode": 401
}
为了统一后端返回值格式,定义了一个统一的JSON结果。与前端约束好,返回的JSON格式,先看success是否为true,如果不对,再看失败的原因。