SpringSecurity+JWT快速构建登录验证,授权

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反序列化后调用一下重新装载权限的方法。,,,,用户凭证并不需要保存

完全解决权限丢失问题,反序列化权限丢失解决

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值