整合SpringSecurity随机加密的无状态认证授权设计的详细讲解

我们平时会使用Md5来做授权认证,不管是session还是jwt,但是如果被别人知道了salt还是很危险的,所以这次就来介绍一个强哈希方法来进行加密,其代表的框架也就是SpringSecurity提供的BCryptPasswordEncoder类。

另外这里将会主要介绍jwt的加密认证授权方式,因为无状态的登录相对于有状态的登录对于服务器的压力更小,并且redis本身不用去存储这个session就更好了。

BCrypt强哈希方法,每次加密的结果都是不同的。

在开始介绍之前,先简单介绍一下基于Token去进行身份验证的方法流程:

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名和密码
  3. 验证成功之后,服务端签发一个token,再将token发送给客户端
  4. 客户端收到token存储起来,比如放在cookie
  5. 客户端每次向服务端请求资源的时候需要待着服务端签发的Token
  6. 服务端收到请求,然后去验证客户端请求里面带着的Token,如果验证成功,就向客户端返回请求到的数据

然后再说一下Token机制相对于有状态的几个优点:

  1. 支持CORS
  2. 无状态
  3. 更适合用CDN:可以通过内容分发网络请求你服务端的所有资料,而服务端只要提供API
  4. 去耦
  5. 更适用于移动应用
  6. CSRF
  7. 性能:一次网络往返时间(通过数据库查询session)比HS256加密时间更长
  8. 不需要为登录页面做特殊处理。

紧接着是jwt的三大组成部分:

  • Header

指明我们使用的是jwt的基本信息,比如加密算法的种类和编码的形式

  • Playload

存放有效信息,这个信息分为三部分:
(1) 标准中注册的声明
(2) 公共的声明
(3) 私有的声明

  • Signature

签证信息也由三部分组成:header(base64后的),payload(base64后的),secret

好了,无聊的话说完了,还是直接上代码:

项目实战:

分为两部分:

(1)首先是最基本的加密认证登录(非JWT知识,可略)

首先导入xml文件

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

然后将SpringSecurity的安全配置文件导入

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //当我们需要具体配置的时候,就把空方法拿出来进行配置

    /**
     * authorizeRequests所有security全注解配置实现的开端,表示开始说明需要的权限。
     * 需要的权限分两部分,第一部分是拦截的路径,第二部分访问该路径需要的权限
     * antMatches表示拦截什么路径,permitAll表示什么url可以通行 
     * anyRequests任何的请求,authenticated认证后才能访问
     * .and().csrf().disable()固定写法,表示csrf拦截失效
     * 
     * @param http
     * @throws Exception
     */

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/**").permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable();
    }
}

上面配置好了最基本的拦截器,然后我来说一下BCrypt加密,也就是这篇文章的重点。和Md5不同,以前我们用Md5的时候,都是直接getPwd然后md5一下,和数据库的去比对,但是这个加密,每次加密的结果都是不一样的,不过幸好他有一个api可以帮助我们完成加密之后的认证。

(2)然后是基于Java的JJWT实现JWT

首先导入依赖:

		<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.6.0</version>
        </dependency>

然后我们来写一个关于jwt的demo,加密与解密。

首先是创建CreateJwt进行加密

	public class CreateJwt {

    public static void main(String[] args) {
        //生成jwt
        JwtBuilder jwtBuilder = Jwts.builder()
                .setId("登录用户id")
                .setSubject("Username")
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256, "salt")
                .setExpiration(new Date(new Date().getTime()+60000));//设置过期时间,单位ms
        //将编码转化成string
        System.out.println(jwtBuilder.compact());

    }

}

然后是parseJwt进行解密

public class ParseJwt {

    public static void main(String[] args) {
        Claims salt = Jwts.parser().setSigningKey("salt")
                .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiLnmbvlvZXnlKjmiLdpZCIsInN1YiI6IlVzZXJuYW1lIiwiaWF0IjoxNTcyNDE4ODQ3LCJleHAiOjE1NzI0MTg5MDd9.Xaayf3FqOmsfmTYItnCkZxv6_eCZH-ab3ORqE5yElds")
                .getBody();

        System.out.println("用户id:"+salt.getId());
        System.out.println("用户登录时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(salt.getIssuedAt()));
        System.out.println("用户过期时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(salt.getExpiration()));
        System.out.println("用户名:"+salt.getSubject());
    }

}


因为我们设置了过期时间,所以我们先运行然后等待一分钟之后用加密后的秘钥进行重新运行。

首先是正常的运行
在这里插入图片描述
等待一分钟后:
在这里插入图片描述

证明我们的加密解密是生效的。

※额外的Demo拓展

由于我们平时进行认证的时候,都是将用户的role和permission一起传给后端,所以往往需要自定义一个传值,也就需要claim方法了。

在这里插入图片描述

在这里插入图片描述

Demo介绍完毕之后,我们可以利用demo进行一个封装,让我们直接使用Jwt加密和解密的api。

  • JWTUtils
@ConfigurationProperties("jwt.config")
public class JwtUtil {

    private String key ;

    private long ttl ;//一个小时

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public long getTtl() {
        return ttl;
    }

    public void setTtl(long ttl) {
        this.ttl = ttl;
    }

    /**
     * 生成JWT
     *
     * @param id
     * @param subject
     * @return
     */
    public String createJWT(String id, String subject, String roles) {
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        JwtBuilder builder = Jwts.builder().setId(id)
                .setSubject(subject)
                .setIssuedAt(now)
                .signWith(SignatureAlgorithm.HS256, key).claim("roles", roles);
        if (ttl > 0) {
            builder.setExpiration( new Date( nowMillis + ttl));
        }
        return builder.compact();
    }

    /**
     * 解析JWT
     * @param jwtStr
     * @return
     */
    public Claims parseJWT(String jwtStr){
        return  Jwts.parser()
                .setSigningKey(key)
                .parseClaimsJws(jwtStr)
                .getBody();
    }

}

然后现在让我们将Jwt融入之前的认证与授权中。

  • 基于JWT的接口授权

我先说一下我们的思路,接下来将会展示一个Admin(管理员)账户认证与授权的案例,因为admin与user级别同理,只是user有很多角色,这个可以自己去指定,而admin这里是定死只有admin角色,所以用admin展示会比较方便。

然后admin角色,肯定无法是手动注册的,肯定是需要老板去给admin角色添加的,但是我们还是写了注册这个接口。

接口也是完成了对于password的加密操作,加密的算法引用的是SpringSecurity的BCryptPasswordEncoder加密方法,这个在上面也介绍过了。

紧接着,进行了admin账户的认证接口的开发,让客户端输入的明文去与mysql的密文通过SpringSecurity的api去对比,如果合格了,就会将role和token传过来,当然,token里也可以通过上面提到的claim进行自定义传值。

然后下面开始进行实施。

首先是admin用户的添加操作:

public void add(Admin admin) {
		admin.setId( idWorker.nextId()+"" );
		//密码加密
		admin.setPassword(encoder.encode(admin.getPassword()));//加密方法
		adminDao.save(admin);
	}

然后,我们利用上面的service逻辑手动制造一个admin用户

在这里插入图片描述

再利用这个数据去模拟登陆,登陆接口如下:

public Admin login(Admin admin) {
		//先根据用户名查询对象。
		Admin adminLogin = adminDao.findByLoginname(admin.getLoginname());
		//然后拿数据库中的密码和用户输入的密码匹配是否相同。
		System.out.println(admin.getPassword());
		System.out.println(adminLogin.getPassword());
		if(adminLogin!=null && encoder.matches(admin.getPassword(), adminLogin.getPassword())){
			//保证数据库中的对象中的密码和用户输入的密码是一致的。登录成功
			return adminLogin;
		}
		//登录失败
		return null;
	}

结果如下

在这里插入图片描述

证明我们登录成功了,那么我们这段token里,也就自带了我们的各种信息(包括role),我们可以添加到HTTP请求头中,进行接口是否可以访问的认证。

假设我们写一个接口,这个接口不管实现业务是什么,但是必须得是admin角色才可以去执行:

Service逻辑:首先我们得获取到请求头,并且针对Http header进行过滤,拿出我们的token,对token去执行我们BCryptPasswordEncoder里面的解密操作,拿出我们的role去进行判断。

代码如下:

/**
	 * 删除 必须有admin角色才能删除
	 * @param id
	 */
	public void deleteById(String id) {
//		String token = (String) request.getAttribute("claims_admin");
//		if (token==null || "".equals(token)){
//			throw new RuntimeException("权限不足!");
//		}
//		userDao.deleteById(id);
		String header = request.getHeader("Authorization");
		if (StringUtils.isEmpty(header)) {
			throw new RuntimeException("请先登录");
		}
		if (!header.startsWith("Bearer ")) {
			throw new RuntimeException("权限不足");
		}
		//得到token
		String token = header.substring(7);
		try {
			Claims claims = jwtUtil.parseJWT(token);
			String role = claims.get("role").toString();
			if (role == null || role.equals("admin")) {
				throw new RuntimeException("权限不足");
			}
		} catch (Exception e) {
			throw new RuntimeException("权限不足");
		}
		userDao.deleteById(id);
	}

在这里插入图片描述

  • 基于JWT的接口授权逻辑优化

之前我们的可以看到deleteById接口的实现方法是,从http请求中,找到Authorization的key 过滤掉Bearer 最后得到token,通过token再parseJwt,得到claim。

但是我们是否可以将这一系列过程封装呢。

事实上SpringMVC就可以做到这一点,也就是Interceptor。但是我们在SpringBoot中该如何让他进行激活呢,那就是将拦截器进行注册。

事实上SpringBoot是有这种注册机制的,也是通过WebMvcConfigurationSupport去实现的。

首先我们对这种SpringBoot的注册机制进行配置,并且做一个基本的拦截。

@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        //注册拦截器要声明拦截的url和拦截的http请求
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/**")  //拦截所有的url
                .excludePathPatterns("/**/login/**");  //除了login

    }
}

然后将我们引入的jwtInterceptor进行一个注入,完成一个基于jwt的拦截政策

@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("经过了拦截器");
        //无论如何放行,具体能不能操作还是在具体的操作中去判断。
        //拦截器只负责把请求头中包含token的令牌进行一个解析验证
        String header = request.getHeader("Authorization");
        if (header!= null && !"".equals(header)) {
            if (!header.startsWith("Bearer ")) {
                throw new RuntimeException("权限不足");
            }
            //得到token
            String token = header.substring(7);
            //对令牌进行检验
            try {
                Claims claims = jwtUtil.parseJWT(token);
                String role = claims.get("role").toString();
                if (role != null || role.equals("admin")) {
                    request.setAttribute("claims_admin", token);
                }
                if (role != null || role.equals("user")) {
                    request.setAttribute("claims_user", token);
                }
            } catch (Exception e) {
                throw new RuntimeException("令牌不正确");
            }
        }
        return true;
    }


}
【课程介绍】     课程目标:             - 有状态登录和无状态登录的区别             - 常见的非对称加密算法和非对称的加密方式             - 老版本只使用jwt进行加密的弊端             - 授权中心的授权流程             - 如何整合网关组件实现jwt安全验证             - 理解什么是公钥什么是私钥      - 深刻理解授权流程什么是有状态? 有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。缺点是什么?- 服务端保存大量数据,增加服务端压力- 服务端保存用户状态,无法进行水平扩展- 客户端请求依赖服务端,多次请求必须访问同一台服务器。什么是无状态? 微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:- 服务端不保存任何客户端请求者信息- 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份带来的好处是什么呢?- 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务- 服务端的集群和状态对客户端透明- 服务端可以任意的迁移和伸缩- 减小服务端存储压力
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值