Spring Security
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 }