概述
JWT简单说明
旧的token使用方式:例如某用户登录成功,用户身份标识信息为
name=zs,id=123456
一般做法是将此信息存入缓存,然后赋予一个编号(一般是UUID)
cf667d3e7ba2fc07aed1b0470810ae4c -> name=zs,id=123456
然后将cf667d3e7ba2fc07aed1b0470810ae4c
返回给前端称为token,请求时附带此数据后台即可得知请求人为zs,编号为123456
JWT使用签名加密技术,可以将用户身份信息加密签名
,然后直接返回给前端,生成的密文数据称为JWS
明文: name=zs,id=123456
加密签名 ↑
↓ 验签解密
JWS: eyJhbGciOiJIUzI1NiJ9.eyJzdWzhmYzIwMWU0Yj
理论上
后台不需要再保存用户的身份标识信息,直接返回给前端就行。。注意头三个字
JWT使用示例
导入依赖
<properties>
<jwt.version>0.11.5</jwt.version>
</properties
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
所见即所得
public class JwtTest {
/**
* 用于加密签名的秘钥,HS256指的是sha256,有多种选择
*/
private Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
/**
* 需要可以将秘钥获取成字符串类型进行持久保存
*/
private String keyForStr = Encoders.BASE64.encode(key.getEncoded());
/**
* 假定为一登录接口,
*
* @return
*/
public String login() {
//登录成功需要颁发一个token,以下为token的内容,根据需要进行构建
//头信息和备注,类型需要为Map<String, Object>,此处为fastjson
JSONObject head = new JSONObject();
head.put("head", "this is head");
JSONObject remarks = new JSONObject();
remarks.put("msg", "this is msg");
return Jwts.builder()
//以下均为可选参数,按需要添加,jwt封装了一些参数标准方便使用
//当然也可以不遵守他的规范....直接搞字符串或者json随便塞到某个方法里也行
//----------------
//头信息
.setHeader(head)
//主体信息
.setSubject("body")
//颁发人
.setIssuer("system")
//受众
.setAudience("张三")
//到期时间,设置会自动进行检查,不通过会报错,此处设置了十秒前到期
// .setExpiration(Date.from(Instant.now().plusSeconds(-10)))
//启用时间,设置会自动进行检查,不通过会报错,此处设置了十秒后启用
// .setNotBefore(Date.from(Instant.now().plusSeconds(10)))
//颁发时间
.setIssuedAt(new Date())
//编号
.setId("1")
//备注
.addClaims(remarks)
//填入key生成token
.signWith(key).compact();
}
/**
* 登录后的接口在需要验证用户身份时将token传回
* 使用jwt进行验证并从token中取出相应信息
*
* @return
*/
public String verify(String token) {
try {
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
//头信息
JwsHeader header = claimsJws.getHeader();
System.out.println(header.get("head"));
//登录存储的除头以外的其他所有信息均从此对象取
Claims claims = claimsJws.getBody();
System.out.println(claims.get("msg"));
//如果登录时Subject存储了用户id,此处就可以直接取id进行下一步操作
return claims.getSubject();
} catch (JwtException e) {
//验证失败会抛此异常
e.printStackTrace();
throw new RuntimeException("token illegal");
}
}
public static void main(String[] args) {
JwtTest jwtTest = new JwtTest();
String jws = jwtTest.login();
System.out.println(jws);
System.out.println(jwtTest.verify(jws));
}
}
SpringSecurity登录授权
基于SpringBoot2.6,依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
主要需要自定义三个类
用户登录信息实体类,需要实现UserDetails
获取用户登录信息的服务,需要实现UserDetailsService
SpringSecurity的拦截路径,认证方式等配置,需要继承WebSecurityConfigurerAdapter
UserDetails的实现
如图,过于简单不贴代码,框出的即为需要实现的方法,从上至下依次为:
用户权限
用户密码
用户名
账户未过期?
账户未锁定?
凭证未过期?
启用状态?
下面几种状态任意一个返回false就会登录失败
UserDetailsService实现
只有一个方法需要实现,与此处关联的代码在Controller里标注
public class UserService implements UserDetailsService {
private static final String ROLE_PRE_FIX = "ROLE_";
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//此处正常需要从库里根据用户名查询信息,并封装成UserDetails返回,此处模拟写死一个
//用户名123,密码pwd(BCrypt后的),角色为admin,权限del
Users user = new Users();
user.setUsername("123");
user.setPassword(new BCryptPasswordEncoder().encode("pwd"));
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(ROLE_PRE_FIX + "admin"));
authorities.add(new SimpleGrantedAuthority("del"));
user.setAuthorities(authorities);
return user;
}
}
配置类WebSecurityConfigurerAdapter
从上至下属性/方法作用依次为
JWT过滤器,可以先不管在下面说明
密码编码器,此处使用BCrypt,可以替换为明文密码(注释里的实例),若替换明文,上面的UserDetailsService里用户密码也不需要进行编码
验证管理器
用来构建身份验证的方法,此处用了上面的UserDetailsService来构建用户身份
配置拦截路径,验证策略
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtTokenFilter jwtTokenFilter;
/**
* 明文密码 NoOpPasswordEncoder.getInstance()
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new UserService());
}
/**
* antMatchers 匹配请求路径
* authenticated 登录用户可访问
* rememberMe 选了“记住我”的用户可访问
* fullyAuthenticated 通过账户密码登录的用户可访问,通过rememberMe自动登录的用户会被要求重新登录才可访问
* denyAll 拒绝访问
* permitAll 完全开放
* anonymous 未登录时访问,登录了不行
* hasIpAddress 请求来源为指定ip地址可访问
* hasAuthority 需要有指定的权限
* hasRole 需要指定的角色
* hasAnyAuthority 指定一组权限,访问者需要至少有其中的一个权限
* hasAnyRole 指定一组角色,访问者需要至少有其中一个角色
* access 满足表达式可访问,写法:access("hasAuthority('1') && hasRole('2')")
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//开启跨域
.cors()
.and()
.formLogin().disable()
//关闭csrf
.csrf().disable()
//声明session为无状态
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//请求路径验证策略
.authorizeRequests()
//此处只配置了一个
.antMatchers("/*/login").permitAll()
//其他均需要登录才能访问
.anyRequest().authenticated();
//将自定义的jwt过滤器器放在UsernamePasswordAuthenticationFilter过滤器之前
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
将SpringSecurity和JWT合并
上面SpringSecurity的配置已经完了,下面是将两个结合起来用
简易版JWT工具类,未使用开头给出的单独JWT实例
@Component
public class JwtBuilder {
private Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
public String buildJws(String data) {
return Jwts.builder()
.setSubject(data)
.signWith(key).compact();
}
public String verify(String token) {
try {
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return claimsJws.getBody().getSubject();
} catch (JwtException e) {
//验证失败会抛此异常
e.printStackTrace();
throw new RuntimeException("token illegal");
}
}
}
自实现一个JWT过滤器
作用:常规cookie+session因为保存了用户会话状态所以可以自动提取识别。而jwt是无状态的鉴权方案所以需要手动提取,UsernamePasswordAuthenticationFilter
里开始验证用户凭证是否有效,所以需要在此之前将用户凭证调出来放入上下文中(上面config配置了此过滤器生效位置)
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
public static Map<String, Users> redis = new HashMap<>(16);
@Autowired
JwtBuilder jwtBuilder;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
if (null != token) {
String userId = jwtBuilder.verify(token);
System.out.println(userId);
//假装从redis里取出来
Users users = redis.get(userId);
UsernamePasswordAuthenticationToken userToken =
new UsernamePasswordAuthenticationToken
(users.getUsername(), users.getPassword(), users.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(userToken);
}
filterChain.doFilter(request, response);
}
}
整个Controller
@RestController
public class UserController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
JwtBuilder jwtBuilder;
@RequestMapping("/user/login")
public String userLogin(@RequestParam("nm") String name, @RequestParam("pwd") String pwd) {
UsernamePasswordAuthenticationToken userToken =
new UsernamePasswordAuthenticationToken(name, pwd);
Authentication authenticate;
try {
//此处经过一系类内部类后会调用至UserDetailsService的load方法
//load方法的入参name就是token里的name
authenticate = authenticationManager.authenticate(userToken);
} catch (AuthenticationException e) {
//验证失败会抛异常
e.printStackTrace();
return "login fail:" + e.getMessage();
}
//成功的话可以获取UserDetailsService方法的返回
Users user = (Users) authenticate.getPrincipal();
//这里存的只是自动生成凭证用,密码不再使用,抹掉用户密码
user.setPassword(null);
String id = UUID.randomUUID().toString();
//假装存在redis里
JwtTokenFilter.redis.put(id, user);
String token = jwtBuilder.buildJws(id);
return "login success:" + token;
}
@RequestMapping("/user/qry")
public String qry() {
return "data";
}
@RequestMapping("/user/del")
@PreAuthorize("hasAuthority('del') && hasRole('admin')")
public String del() {
return "is del";
}
}
简单测试,登录返回的jwt
测试授权功能。如果在UserDetailsService里删掉两个权限其中一个则del不能访问。SpringSecurity的权限和角色是一体的,用String的开头区分,字符串开头是ROLE_即是角色,否则就是权限
最后,理论上来说可以将UserDetails直接序列化然后放入JWT,将生成的JWS直接在JwtTokenFilter里解密提取然后反序列化,可以完全做到服务端不存储用户凭证。实际操作时发现jws解密再反序列化时UserDetails的权限列表丢失,所以当前用户凭证仍然需要保存。至少需要存用户权限
权限列表的补充说明
上述对于权限丢失说明错误。权限丢失实际上是因为GrantedAuthority(实例SimpleGrantedAuthority)不能反序列化导致,最好的解决方案是看一下GrantedAuthority自己写一个实现。此处提供一个最简单的处理方法(使用的是SimpleGrantedAuthority)
修改UserDetails的实现类
将实际的权限列表集合置为不使用序列化,并取消set方法,新建一个保存权限字符串的集合,修改登录将取出的权限列表直接以String存入新建的集合,然后再修改JwtTokenFilter中步骤将UserDetails反序列化后调用一下重新装载权限的方法。,,,,用户凭证并不需要保存
完全解决权限丢失问题,反序列化权限丢失解决
最后
以上就是紧张蛋挞为你收集整理的SpringSecurity+JWT快速构建登录验证,授权的全部内容,希望文章能够帮你解决SpringSecurity+JWT快速构建登录验证,授权所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复