Spring Security

neweastsun 2020-06-04

Spring Security

Spring家族的安全管理框架,竞品是Shiro。

虽然Security功能比Shiro强大,但Spring Boot出现之前,Security的整合比较麻烦,使得大部分项目选择使用Shiro。

Spring Boot对于Spring Security提供了自动化配置方案,常常配合使用。

Bcrypt加密

BCryptPasswordEncoder类实现了Bcrypt加密。

Bcrypt加密使用单向hash算法,每次加密结果不一样,因此无解密功能,即使数据库泄漏也很难破解密码。

BCryptPasswordEncoder可以加密(encode)和匹配(matches)。

实验

添加依赖

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

编写配置类

添加Spring Security依赖后,访问任何URL都会进入自带的Login Page页面。

为了查看BCrypt加密效果,需要添加一个配置类,使得所有地址都可以匿名访问。

package com.ah.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.*;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// authorize:授权
		// authenticated:验证
		// csrf跨站点请求伪造(Cross—Site Request Forgery)
		http.authorizeRequests().antMatchers("/**").permitAll()
		.anyRequest().authenticated().and().csrf().disable();
	}
}

编写启动类,配置BCryptPasswordEncoder的@Bean

package com.ah;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@SpringBootApplication
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

	@Bean
	public BCryptPasswordEncoder encoder() {
		return new BCryptPasswordEncoder();
	}
}

测试加密(regist)和密码匹配(login)

package com.ah.security;

import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import io.jsonwebtoken.Claims;

@RestController
public class UserController {
	@Autowired
	BCryptPasswordEncoder encoder;
	static String DUMMY_DB_PWD = "";

	@GetMapping("/BCrypt/regist/{pwd}")
	public String regist(@PathVariable String pwd) {
		pwd = encoder.encode(pwd);
		// DUMMY:存入数据库
		DUMMY_DB_PWD = pwd;
		return "pwd = " + DUMMY_DB_PWD;
		// http://127.0.0.1:8080/BCrypt/regist/123
	}

	@GetMapping("/BCrypt/login/{pwd}")
	public String login(@PathVariable String pwd) {
		// DUMMU:从数据库根据用户名,取登录用户信息
		if (encoder.matches(pwd, DUMMY_DB_PWD)) {
			return "login Success";
		} else {
			return "login Fail";
		}
		// http://127.0.0.1:8080/BCrypt/login/123
	}
}

JWT

Json Web Token,是一种规范,定义了基于Json和Token的身份验证机制

JWT使得信息可以在客户端和服务器之间可靠传递,适用于分布式站点的单点登录。

JWT基于Token验证,在服务端不存储用户的登录记录,流程如下:

  1. 客户端发送登录请求
  2. 服务器验证成功后,发送一个Token给客户端。
  3. 客户端存储Token,可以存在Cookie里。
  4. 客户端每次向服务器发送请求时,都要携带该Token。
  5. 服务器收到请求,对Token进行验证,验证成功就继续处理。

Token机制的好处:

  • 支持跨域访问(Cookie不支持)
  • 支持多平台(移动平台支持Cookie)
  • 不用考虑scrf(跨站点请求伪造)。
  • 无状态,即服务器不存储登录信息
  • 解耦
  • 性能:相对session存储登录信息的形式,JWT性能更好。
  • 标准化:JWT已被多种技术所支持(Java、.NET、Python、PHP、Ruby等)

JWT是字符串,由三部分组成:头部header、载荷playload、签名signature。如:

eyJhbGciOiJIUzI1NiJ9.
eyJqdGkiOiIwMDciLCJzdWIiOiLpgqblvrciLCJpYXQiOjE1OTEyMDE0MjMsImV4cCI6MTU5MTIwNTAyMywicm9sZSI6ImFkbWluIn0.
Rd0IOiEoLodeZDDikS7to4JakThtYOCUtCJ3alCHjQM
  • header:描述基本信息,如类型或签名算法等。

  • playload:载荷,存放有效信息,包括标准声明、公共声明、私有声明。

  • signature:签名。

JJWT实现JWT

<!-- JJWT:Token认证 -->
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt</artifactId>
	<version>0.9.1</version>
</dependency>
<!-- 如果测试出错,则加入此包 -->
<dependency>
	<groupId>javax.xml.bind</groupId>
	<artifactId>jaxb-api</artifactId>
</dependency>

编写测试类(其实就是工具类,后面用)

  • 新建JWT字符串
    • 注意自定义属性(claim:声明)
  • 解析JWT字符串
package com.ah.security;
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import io.jsonwebtoken.*;
@Configuration
public class JWTUtil {
	@Value("andy")
	private String key;
	@Value("3600000") // 60min*60s*1000ms
	private long ttl;// time to live,生存时间

	public String createJWT(String _id, String _subject, String _role) {
		JwtBuilder builder = Jwts.builder();
		builder.setId(_id);
		builder.setSubject(_subject);
		builder.setIssuedAt(new Date());// 发行时间
		builder.signWith(SignatureAlgorithm.HS256, key);// 签名
		long nowMillis = System.currentTimeMillis();
		builder.setExpiration(new Date(nowMillis + ttl));// 失效时间
		// 自定义属性(claim:声明)
		builder.claim("role", _role);
		String strJWT = builder.compact();// 把…紧压在一起
		return strJWT;
	}

	public Claims parserJWT(String strJWT) {
		JwtParser parser = Jwts.parser();
		parser.setSigningKey(key);
		Jws<Claims> parseClaimsJws = parser.parseClaimsJws(strJWT);
		Claims body = parseClaimsJws.getBody();
		return body;
	}

	public static void main(String[] args) {
		JWTUtil jwt = new JWTUtil();
		jwt.key = "andy";
		jwt.ttl = 3600000;
		String str = jwt.createJWT("007", "邦德", "admin");
		System.out.println(str);
		Claims body = jwt.parserJWT(str);
		System.out.println("Id=" + body.getId());
		System.out.println("Subject=" + body.getSubject());
		System.out.println("IssuedAt=" + body.getIssuedAt());
		System.out.println("Expiration=" + body.getExpiration());
		System.out.println("role=" + body.get("role"));
	}
}

应用:鉴权

  • 使用拦截器对用户进行鉴权。
  • 登录页面不拦截。
  • 用户登录,获得token。
  • 用户发送一个Delete请求,该请求被鉴权。

拦截器

package com.ah.security.interceptor;
import javax.servlet.http.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import com.ah.security.JWTUtil;
import io.jsonwebtoken.Claims;

@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("---preHandle---拦截---" + request.getRequestURI());

		String jwt = request.getHeader("Authorization");
		if (jwt != null) {
			System.out.println(jwt);
			// 解析JWT
			Claims claims = jwtUtil.parserJWT(jwt);
			// 根据role,设置属性
			String role = (String) claims.get("role");
			if ("admin".equalsIgnoreCase(role)) {
				request.setAttribute("role_admin", claims);
			} else if ("user".equalsIgnoreCase(role)) {
				request.setAttribute("role_user", claims);
			}
		}
		return true;
	}
}

拦截器配置类

package com.ah.security.interceptor;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class ApplicationConfig extends WebMvcConfigurationSupport {
	@Autowired
	private JWTInterceptor jwtInterceptor;

	@Override
	protected void addInterceptors(InterceptorRegistry registry) {
		InterceptorRegistration registration = registry.addInterceptor(jwtInterceptor);
		registration.addPathPatterns("/**");// 全部拦截
		registration.excludePathPatterns("/**/login/**");// 不拦截
	}
}

配置器中加两个方法

@Autowired
	private JWTUtil jwtUtil;

	@GetMapping("/jwt/login/{pwd}")
	public String jwt_login(@PathVariable String pwd) {
		// DUMMU:从数据库根据用户名,取登录用户信息
		DUMMY_DB_PWD = encoder.encode(pwd);
		if (encoder.matches(pwd, DUMMY_DB_PWD)) {
			System.out.println("登录成功");
			// 登录成功后,返回给用户jwt(这里直接返回字符串,但实际项目中一般会封装到结果对象中)
			String jwt = jwtUtil.createJWT("DUMMY_ID", pwd, "admin");
			return jwt;
		} else {
			System.out.println("登录失败");
			return "login Fail(JWT)";
		}
		// http://127.0.0.1:8080/jwt/login/123
	}

	@GetMapping("/jwt/delete")
	public String adminDelete(HttpServletRequest request) {
		Claims claims = (Claims) request.getAttribute("role_admin");
		if (claims == null) {
			return "Permission denied";
		}
		// do something
		return "Delete Success";
		// http://127.0.0.1:8080/jwt/delete
        // postman测试,Headers中新建Authorization,value=jwt
	}

相关推荐