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验证,在服务端不存储用户的登录记录,流程如下:
- 客户端发送登录请求
- 服务器验证成功后,发送一个Token给客户端。
- 客户端存储Token,可以存在Cookie里。
- 客户端每次向服务器发送请求时,都要携带该Token。
- 服务器收到请求,对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
}