整合SpringSecurity随机加密的无状态认证授权

平时会使用MD5来做授权认证,不管是Session还是JWT,但是如果被别人知道了salt是很危险的,因此这里来介绍一个强哈希方法来进行加密,代表的框架就是SpringSecurity提供的BCryptPasswordEncoder类。

此外本篇文章还会主要介绍一下JWT的加密认证授权方式,是因为无状态的登录相对于有状态的登录对于服务器的压力会更小,并且Redis本身不用去存储这个Session。

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

介绍一下基于Token进行身份验证的流程:

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

简单说一下Token机制相对于Cookie机制的好处:

  1. 支持跨域访问
  2. 无状态
  3. 更适用CDN:可以通过内容分发网络请求服务端的所有资料,服务端只需要提供API即可
  4. 去耦
  5. 更加适用于移动应用
  6. CSRF
  7. 性能:一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算的Token验证和解析要费时得多
  8. 不需要为登录页面做特殊处理

JWT组成

  1. 头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等,也可以被表示成一个json对象

  1. 载荷(playload)

载荷就是存放有效信息的地方,有效信息包含三个部分:
(1)、标准中注册的声明(建议但不强制使用)
(2)、公共的声明:可以添加任何信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可以解密
(3)、私有的声明

  1. 签证(signature)

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

项目实战

(1)、加密认证
导入maven依赖:

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

SpringSecurity安全配置文件

/**
 * 安全配置类
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         *  authorizeRequests():security全注解配置的开端,表示开始说明需要的权限。第一部分:拦截的路径 第二部分:访问该路径需要的权限
         *  antMatchers:拦截什么路径  permitAll:任何权限都可以访问,放行所有
         *  anyRequest任何的请求,authenticated认证之后才可以访问
         *  and().csrf().disable()固定写法,使csrf失效
         */
        http.authorizeRequests()
            .antMatchers("/**").permitAll()
            .anyRequest().authenticated()
            .and().csrf().disable();
    }
}

这样就配置完成了最基本的拦截器,说一下BCryptPasswordEncoder加密和MD5不一样,使用MD5时候都是直接getPassword()然后MD5和数据库去对比,但是这个加密每次的结果都是不一样的,但是有一个API可以帮助完成加密之后 的认证。
(2)、基于Java的JJWT实现JWT
同样还是先导入maven依赖

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

首先先来看看其实如何进行加密的,先来写一个小的demo
创建create_jwt进行加密

/**
 * 创建JWT测试类
 */
public class create_jwt {

    public static void main(String[] args) {
        JwtBuilder jwtBuilder = Jwts.builder()
                .setId("521")
                .setIssuedAt(new Date()).setSubject("周晓晨")
                .signWith(SignatureAlgorithm.HS256, "zxczxc")
                .setExpiration(new Date(new Date().getTime() + 60000))
                .claim("role", "admin");

        System.out.println(jwtBuilder.compact());
    }
}

parse_jwt进行解密:

/**
 * 解析JWT测试类
 */
public class parse_jwt {

    public static void main(String[] args) {

        try {

            Claims claims = Jwts.parser().setSigningKey("zxczxc")
                    .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MjEiLCJpYXQiOjE1NzMzNzc3ODIsInN1YiI6IuWRqOaZk-aZqCIsImV4cCI6MTU3MzM3Nzg0Mn0.9rI4VBzyvvIaFZ8cghACQm4zryetitBgE-C5HZLryOI")
                    .getBody();

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

        }catch (Exception e){
            throw new RuntimeException("JWT TOKEN已经过期");
        }
    }
}

在这里插入图片描述
在这里插入图片描述
创建jwt因为设置了过期时间,因此一分钟之后密钥生效。

微服务鉴权

1、首先封装一个创建JWT和解密的工具类

/**
 * JWT工具类
 */
@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
     */
    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
     */
    public Claims parseJWT(String jwtStr){

        return  Jwts.parser()
                .setSigningKey(key)
                .parseClaimsJws(jwtStr)
                .getBody();
    }
}

2、基于JWT的授权
下面会展示一个Admin(管理员)账户认证与授权的案例,因为admin和user级别实现起来相同,user有很多角色可以去指定,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(bCryptPasswordEncoder.encode(admin.getPassword()));
		adminDao.save(admin);

添加几个用户
在这里插入图片描述
admin登录:

	/**
	 * Admin Login
	 */
	public Admin login(Admin admin) {

		//根据用户名查询对象
		Admin admin_login = adminDao.findByLoginname(admin.getLoginname());

		//数据库中的密码和用户输入的密码是一致的
		if (admin_login != null && bCryptPasswordEncoder.matches(admin.getPassword(), admin_login.getPassword())) {
			return admin_login;
		}
		return null;
	}

测试结果如下:
在这里插入图片描述
admin登录成功,token中包含了各种的信息(包含角色),可以添加到HTTP请求头中,进行接口是否可以访问的认证。

假设我们写一个接口必须是admin角色才可以去执行:

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

	/**
	 * 删除 必须有admin角色才能删除
	 */
	public void deleteById(String 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);
	}

在这里插入图片描述
3、基于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) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/**/login/**");
    }
}

编写拦截器

/**
 * 拦截器
 */
@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)) {
            //请求头包含Authorization头信息,对其进行解析
            if (header.startsWith("Broker ")) {
                //获取token
                String token = header.substring(7);
                try {
                    Claims claims = jwtUtil.parseJWT(token);
                    String roles = (String) claims.get("roles");
                    if (roles != null && roles.equals("admin")) {
                        request.setAttribute("claims_admin", token);
                    }
                    if (roles != null && roles.equals("user")) {
                        request.setAttribute("claims_user", token);
                    }
                }catch (Exception e){
                    throw new RuntimeException("令牌不正确");
                }
            }
        }
        return true;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值