前言
关于系统最终想实现的功能:使用token来实现登录校验,用户登录后拿到token,然后将token放入httpHeader,之后每次接口请求都携带token,验证成功才可进行正常访问流程
储备知识
token与 jwt (JSON Web Token)介绍
JWT 的原理
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2018年7月1日0点0分"
}
JWT 的数据结构
它是一个很长的字符串,中间用点(
.
)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。
JWT 的三个部分依次如下。
Header(头部)
Payload(负载)
Signature(签名)
Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,
alg
属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);
typ
属性表示这个令牌(token)的类型(type),JWT 令牌统一写为
JWT
。
最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。
Payload
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(
.
)分隔,就可以返回给用户。
JWT 工具类
有很多常见的工具类,我这边用的是这个
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
spring security简介
介绍的文章一抓一大把,这边主要说一下他的几个核心东西
用户认证(Authentication)
是验证您的身份的凭据(例如用户名/用户ID和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。
用户授权(Authorization)
发生在 **Authentication(认证)**之后。授权嘛,光看意思大家应该就明白,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。
过滤器链
核心组件
AuthenticationManager
SecurityContextHolder
PasswordEncoder
UserDetails
UserDetailsService
BasicAuthenticationFilter
AuthenticationEntryPoint
登录流程图
集成流程
集成spring security
maven依赖
springboot版本 2.7.3 springsecurity 版本 5.7.3
<!-- spring-boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/>
</parent>
<dependencies>
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
WebSecurityConfig 配置
注意,springsecurity 5.7之后配置方式已经优化,无需再使用继承式的配置,直接bean方式配置即可
@EnableWebSecurity
@EnableConfigurationProperties(AuthProperties.class)
public class WebSecurityConfig {
@Autowired
private SysUserService sysUserService;
@Autowired
private AuthProperties authProperties;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JWTService jwtService;
@Autowired
private CacheManager cacheManager;
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// 基于 token,不需要 csrf
.csrf().disable()
// 基于 token,不需要 session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 下面开始设置权限
.authorizeRequests(authorize -> authorize
.antMatchers(authProperties.getPermitStatic().toArray(new String[0])).permitAll()
.antMatchers(authProperties.getPermitMethod().toArray(new String[0])).permitAll()
// 其他地址的访问均需验证权限
.anyRequest().authenticated())
.addFilter(new JWTAuthenticationFilter(authenticationManager, sysUserService, jwtService, userCache()))
.exceptionHandling().authenticationEntryPoint(new UserAuthenticationEntryPoint()).and()
// 认证用户时用户信息加载配置,注入springAuthUserService
.userDetailsService(sysUserService).build();
}
/**
* 获取AuthenticationManager(认证管理器),登录时认证使用
* @param authenticationConfiguration
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* 密码明文加密方式配置(使用国密SM4)
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new SM4PasswordEncoder();
}
@Bean
UserCache userCache(){
Cache ca = cacheManager.getCache("userCache");
return new SpringCacheBasedUserCache(ca);
}
授权相关配置文件 AuthProperties
@Data
@ConfigurationProperties(prefix = "lc.security")
public class AuthProperties {
private JWT jwt;
private List<String> permitStatic;
private List<String> permitMethod;
@Data
public static class JWT{
private Claims claims = new Claims();
private String authHeader;
private String secret;
private Type type = Type.RANDOM;
public void setAuthHeader(String authHeader) {
this.authHeader = authHeader;
}
public String getAuthHeader() {
return authHeader;
}
public Claims getClaims() {
return claims;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getSecret() {
return secret;
}
public void setType(Type type) {
this.type = type;
}
public Type getType() {
return type;
}
public enum Type {
RANDOM, FOREVER
}
@Setter
@Getter
public static class Claims {
private String issuer = "AppName";
private String audience = "Web";
private String subject = "Auth";
private Long expirationTimeMinutes = 60L;
}
}
配置文件 application-security.yaml
lc:
security:
#静态资源放行
permit-static:
- /*.html
- /*.html
- /favicon.ico
- /**/*.html
- /**/*.css
- /**/*.js
- /**/*.png
- /**/*.jpg
- /**/*.ttf
- /**/*.woff
- /**/*.wav
- /**/*.gif
- /swagger-ui.html
#方法放行
permit-method:
- /swagger-resources
- /v2/api-docs
- /v3/api-docs
- /api/v1/sys/auth/login
jwt:
auth-header: Authorization
secret: mySecret
type: forever
claims:
issuer: LC
audience: Web
subject: Auth
expiration-time-minutes: 3000
异常处理UserAuthenticationEntryPoint
public class UserAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.OK.value());
response.getWriter().write(JSONObject.toJSONString(ReturnVO.failed("登录信息不正确!")));
response.getWriter().flush();
}
}
过滤器JWTAuthenticationFilter
@Slf4j
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
private final SysUserService userService;
private final JWTService jwtService;
private final UserCache userCache;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager, SysUserService userService, JWTService jwtService, UserCache userCache) {
super(authenticationManager);
this.userService = userService;
this.jwtService = jwtService;
this.userCache = userCache;
}
@SneakyThrows
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = jwtService.getToken(request);
String tokenHeader = request.getHeader("Authorization");
// 如果请求头中没有Authorization信息则直接放行了
if (!StringUtils.hasLength(tokenHeader)) {
chain.doFilter(request, response);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
String username = jwtService.validateToken(token);
if (!StringUtils.hasLength(username)) {
log.error("从token中未获取到用户名, token:{}, URI:{}", token, request.getServletPath());
chain.doFilter(request, response);
return;
}
//从缓存中验证token的存在性
UserInfo user = (UserInfo) userCache.getUserFromCache(username);
if (null == user) {
try {
user = (UserInfo) userService.loadUserByUsername(username);
userCache.putUserInCache(user);
} catch (UsernameNotFoundException e) {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(HttpStatus.OK.value());
response.getWriter().write(JSONObject.toJSONString(ReturnVO.failed(e.getMessage())));
response.getWriter().flush();
return;
}
}
// 如果从持久化存储中仍未查到,则执行后续操作,最后返回用户不存在信息到前端
if (null != user) {
// 清空“密码”属性
// 创建验证通过的令牌对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
// 设置令牌到安全上下文中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
JWTService 与 JWTServiceImpl
public interface JWTService {
/**
* 签名生成
* @param username
* @return
*/
String generateToken(String username);
/**
* 签名检验
* @param token
* @return
*/
String validateToken(String token);
/**
* 签名查询
* @param request
* @return
*/
String getToken(HttpServletRequest request);
}
@Slf4j
@Service
public class JWTServiceImpl implements JWTService {
private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;
private AuthProperties properties;
public JWTServiceImpl(AuthProperties properties) {
this.properties = properties;
}
private Claims getAllClaims(String token) throws AuthTokenException {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(properties.getJwt().getSecret())
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
throw new AuthTokenException(e.getMessage());
}
return claims;
}
private Date generateExpirationDate() {
return new Date(new Date().getTime() + properties.getJwt().getClaims().getExpirationTimeMinutes() * 60 * 1000);
}
private String getAuthHeader(HttpServletRequest request) {
return request.getHeader(properties.getJwt().getAuthHeader());
}
@Override
public String generateToken(String username) {
return Jwts.builder()
.setIssuer(properties.getJwt().getClaims().getIssuer())
.setSubject(username)
.setAudience(properties.getJwt().getClaims().getAudience())
.setIssuedAt(new Date())
.setExpiration(generateExpirationDate())
.signWith(SIGNATURE_ALGORITHM, properties.getJwt().getSecret())
.compact();
}
@Override
public String validateToken(String token){
Claims allClaims = null;
try {
return getAllClaims(token).getSubject();
} catch (AuthTokenException e) {
log.error(e.getMessage(), e);
}
return null;
}
@Override
public String getToken(HttpServletRequest request) {
return getAuthHeader(request);
}
}
SysUserService
@Slf4j
@Service
public class SysUserService implements UserDetailsService {
@Autowired
private SysUserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfo userInfo = userMapper.getUserInfoByUsername(username);
ParamAssert.notNull(userInfo, "用户不存在!");
return userInfo;
}
}
用户信息
@Data
@ApiModel("用户")
@EqualsAndHashCode(callSuper = true)
public class UserInfo implements UserDetails {
@ApiModelProperty(notes = "用户名")
private String username;
@ApiModelProperty(notes = "姓名")
private String name;
@ApiModelProperty(notes = "编码")
private String code;
@ApiModelProperty(notes = "密码")
private String password;
@ApiModelProperty(notes = "是否启用:true-启用,false-停用")
private boolean enabled = true;
private List<RoleAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
}
测试
登录接口
启动项目,放开登录接口,登录
返回结果
{
"code": 200,
"message": "SUCCESS",
"data": {
"token": "eyJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJMQy1NSU5GQU5HIiwic3ViIjoiYWRtaW4iLCJhdWQiOiJXZWIiLCJpYXQiOjE2NjUyMjAzODMsImV4cCI6MTY2NTQwMDM4M30.MhqQl79CgevBw2zeDuL2tsxgZaUe43e16-kw0aWMfCD5Hs9NI_D0dlwwvvr0znlORf6y5eyzyao8EqVIv09URQ"
}
}
登陆失败提示
版权归原作者 ricardo.M.Yu 所有, 如有侵权,请联系我们删除。