Spring Security + JWT + Swagger2 登录验证一套流程
首要是三个结构的集成装备,以及各个独立的装备(首要是 JWT + Security 的登录验证)。
流程:
- 构建 Spring Boot 根本项目,预备数据库表 User —— 用于存放登录实体类信息。
- 装备 Security 和 Swagger2 环境,保证没有什么问题。
- 构建
RespBean——公共回来实体类
,JwtTokenUtil——JWT token 东西类
,User——登录实体类
- 让 User 完结
UserDetails
接口,重写部分办法。 - 装备 Security 完结重写
UserDetailsService
办法,以及PasswordEncoder——暗码凭据器
并加上@Bean
注解。这两个首要用于设置 Security 的认证。 - 构建
jwtAuthenticationTokenFilter
类——自定义 JWT Token 阻拦器,并在SecurityConfig
的授权办法中增加此阻拦器。 - 在
Swagger2Config
装备类中,装备有关 Security 的 Token 认证。 - 启动项目查看代码是否精确。
1. 构建 Spring Boot 根本项目,预备数据库——User
项目子模块:authority-security
,父模块已引进 Springboot依靠2.3.0
1.1 导入依靠
<dependencies>
<!-- web 依靠 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok 依靠 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- mysql 依靠 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis-plus 依靠 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<!-- swagger2 依靠 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<!-- swagger 第三方 UI 依靠 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 依靠 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- commons-pool2 目标池依靠 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
构建数据库表:user
create table user(
id int primary key auto_increment,
username varchar not null,
password varchar not null,
info varchar(200),
enabled tinyint(1) default 1
)
insert into user values(default,"admin","$2a$10$Himwt.wu3MPOLnNQ9YUH8O2quxgi7bMuomiNeFsVKRay87.qG5dgy","办理员 info ...",default)
username:admin;password:123
装备 application.yml
文件参数:
server:
port: 8082
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/dbtest16?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: admin
password: admin
hikari:
# 衔接池名字
pool-name: DateHikari
# 最小闲暇衔接数
minimum-idle: 5
# 闲暇衔接存活最大事件,默许10分钟(600000)
idle-timeout: 180000
# 最大衔接数:默许 10
maximum-pool-size: 10
# 从衔接池回来的衔接主动提交
auto-commit: true
# 衔接最大存活时刻,0 表示永久存活,默许 1800000(30 min)
max-lifetime: 1800000
# 衔接超时事件 30 s
connection-timeout: 30000
# 测试衔接是否可用的查询语句
connection-test-query: SELECT 1
# MP 装备
mybatis-plus:
# 装备 Mapper 映射文件
mapper-locations: classpath*:/mapper/*Mapper.xml
# 实体类的别号包
type-aliases-package: com.cnda.pojo
configuration:
# 主动驼峰命名
map-underscore-to-camel-case: false
# MyBatis 的 SQL 打印是办法接口所在的包
logging:
level:
com.cnda.mapper: debug
# JWT 装备
jwt:
# JWT 存储的恳求头
tokenHeader: Authorization
# JWT 加密运用的密钥
secret: test-cnda-secret
# JWT 的有用时刻 (60*60*24)
expiration: 604800
# JWT 负载中拿到最初 规则
tokenHead: Bearer
User 实体类代码:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
private String info;
private Boolean enabled;
}
2. 装备 Security 和 Swagger2 的装备
先装备好这两个保证没有什么问题,因为重点是 JWT,这两个装备比较简单,当搭配了 JWT 之后,Swagger2 也需求与两者集成一些装备,这个后边再说,现在只装备根本设置。
2.1 装备 SecurityConfig
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/hello",
// 下面是对静态资源以及 swagger2 UI 的放行。
"/css/**",
"/js/**",
"/img/**",
"/index.html",
"favicon.ico",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/ws/**"
);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
上面运用 WebSecurity
放行了 /hello
恳求,在 LoginController
中。
@RestController
public class LoginController {
@RequestMapping("/hello")
public String hello(){
return "Hello Word!";
}
}
这意味除了 localhost:8082/hello
会被放行,其他恳求都会被 Security 阻拦重定向到 /login
(这个恳求 Security 内部已经完结了包括相关页面)。
2.2 装备 Swagger2Config
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo()) // 装备 apiInfo
.select() // 选择那些途径和api会生成document
.apis(RequestHandlerSelectors.basePackage("com.cnda.controller")) // // 对哪些 api进行监控,RequestHandlerSelectors.basePackage 依据包扫描
.paths(PathSelectors.any()) // 对一切途径进行监控
.build();
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("在线接口文档")
.description("在线接口文档")
.contact(new Contact("cnda","http://localhost:8082/doc.html","xxx@xxx.com"))
.build();
}
}
运转作用:
修正一下 Rustful 风格,并加了一个 /hello1
恳求,不放行,打印内容相同。
能够看到 Security 和 Swagger2 根本装备完结。
3. 构建 JWT 东西类、公共呼应目标
JWT 东西类首要用于生成 JWT,判别 JWT 是否有用,改写 JWT 等办法。
公共呼应目标——RespBean
,回来的都已 JSON 格局回来。
3.1 JwtUtil
@Component
public class JwtUtil {
// 预备两个存放在荷载的内容
private static final String CLAIM_KEY_SUB = "sub";
private static final String CLAIM_KEY_CREATE = "ibt";
// 提取 application.yml 中 JWT 的参数:
// 1. expiration Long
@Value("${jwt.expiration}")
private Long expiration;
// 2. secret String
@Value("${jwt.secret}")
private String secret; // 密钥
// 依据用户名构建 token
public String foundJWT(UserDetails userDetails) {
String username = userDetails.getUsername();
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_SUB, username);
claims.put(CLAIM_KEY_CREATE, new Date());
return foundJWT(claims);
}
// 依据荷载 map 构建 token
private String foundJWT(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(getExpiration()) // 过期时刻
.signWith(SignatureAlgorithm.HS512, secret) // 设置签名算法和密钥
.compact();
}
// 判别 token 是否有用
public boolean validateToken(String token,UserDetails userDetails){
// 从 token 中获取 username 与 userDetails 中的username 对比
String username = getUsernameInToken(token);
// 判别 username 是否共同以及 token 是否过期
return username.equals(userDetails.getUsername()) && !isExpired(token);
}
// 判别 token 是否过期
// true 过期 false 没过期
private boolean isExpired(String token) {
Date expiration = getClaimsInToken(token).getExpiration();
return expiration.before(new Date());
}
// 从 token 中提取荷载信息
public Claims getClaimsInToken(String token){
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
e.printStackTrace();
}
return claims;
}
// 从 token 中提取用户名信息
public String getUsernameInToken(String token){
String username;
try {
username = getClaimsInToken(token).getSubject();
}catch (Exception e){
username = null;
}
return username;
}
// token 是否能改写
public boolean tokenCanRef(String token){
return !isExpired(token); // 有用地 token 才干被改写
}
// 改写 token
public String refToken(String token){
Claims claimsInToken = getClaimsInToken(token);
claimsInToken.put(CLAIM_KEY_CREATE,new Date());
return foundJWT(claimsInToken);
}
// 设置过期时刻
private Date getExpiration() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
}
3.2 RespBean 公共回来目标
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RespBean {
private long code;
private String message;
private Object obj;
/**
* 回来呼应成果
*/
private static RespBean result(long code, String message, Object obj) {
return new RespBean(code, message, obj);
}
/*
回来成功呼应
*/
public static RespBean success(String message) {
return result(200, message, null);
}
/*
回来成功呼应以及数据体
*/
public static RespBean success(String message, Object obj) {
return result(200, message, obj);
}
/*
回来过错呼应
*/
public static RespBean error(String message) {
return result(500, message, null);
}
}
4. 让 User 实体类完结 UserDetails 的办法成为 Security 验证的用户中心主体
因为 Security 结构的性质,自定义授权和认证时,一般情况下会自定义 UserDetails。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private String info;
private Boolean enabled;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { // 权限角色
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() { // 这儿数据库完结了该字段,直接用即可
return this.enabled;
}
}
5. 重写 UserDetailsServer 和 PasswordEncoder
5.1 重写 UserDetailsServer
这个类就只有一个办法:
loadUserByUsername(UserDetails details)
,该办法用于依据用户名加载用户信息,用作于 Security 的后续认证,同时也能够用一个类去完结该接口,这儿为了便利,同时也是 Lambda 表达式。
留意:这儿的 UserMapper
没有代码展现了,就一个依据用户名查询用户信息的 SQL。
@Resource
private UserMapper mapper;
@Bean
@Override
protected UserDetailsService userDetailsService() {
return username -> {
User user = mapper.find(username);
if (user!=null){
return user;
}
throw new UsernameNotFoundException("用户名或暗码不正确");
};
}
5.2 PasswordEncoder——暗码凭据器
这个类首要用于验证表单提交的暗码是否和 重写之后的 UserDetailsServer 得到的 UserDetails 中的加密暗码共同。
@Bean
public PasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
5.3 装备到 SecurityConfig 的认证中
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(encoder());
}
6. 装备 JWT 的阻拦器
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService service;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 获取恳求头中的指定的值
String headerToken = httpServletRequest.getHeader(tokenHeader);
// 保证 header中的 token 不为 null,且以指定字串最初——Bearer
if (headerToken!=null && headerToken.startsWith(tokenHead)){
// 截取有用 token
String jwtToken = headerToken.substring(tokenHead.length());
String username = jwtUtil.getUsernameInToken(jwtToken);
// 判别 UserDetails 中的用户主体是否为null
if (username!=null && SecurityContextHolder.getContext().getAuthentication() == null){
// SecurityContextHolder.getContext().getAuthentication() == null 代表着此刻 Security 中没有登录的用户主体
// 此刻能够运用有用地 jwtToken 进行用户认证
UserDetails userDetails = service.loadUserByUsername(username);
// 判别 token 是否有用
if (jwtUtil.validateToken(jwtToken,userDetails)){
// 如果有用则运用 token 中的信息进行登录
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());
// 依据恳求设置 Details,包含了部分恳求信息和主体信息。具体作用不清楚...坑
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
// 将 authenticationToken 设置到 SecurityContext 中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
6.1 将 JWT 阻拦器设置到 SecurityConfig 的授权办法中。
@Override
protected void configure(HttpSecurity http) throws Exception {
// 因为我们运用的是 JWT 令牌的方式来验证用户,所以能够将 csrf 防御封闭
// JWT 能有用避免 csrf 攻击,强行运用 csrf 可能导致令牌走漏
http.csrf()
.disable()
// 依据 token,不需求运用 Session 了
.sessionManagement() // Session 办理
// 办理 Session 创立战略
// ALWAYS, 总是创立HttpSession
// NEVER, 只会在需求时创立一个HttpSession
// IF_REQUIRED, 不会创立HttpSession,但如果它已经存在,将能够运用HttpSession
// STATELESS; 永久不会创立HttpSession,它不会运用HttpSession来获取SecurityContext
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests() // 授权恳求
// 除了上面的恳求,其他一切恳求都需求认证
.anyRequest()
.authenticated()
.and()
// 制止缓存
.headers()
.cacheControl();
// 自定义阻拦器 JWT 过滤器
http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); // 将过滤器依照一定顺序参加过滤器链。
}
@Bean
public JwtTokenFilter jwtTokenFilter() {
return new JwtTokenFilter();
}
7. 完善 LoginController 恳求,运转项目。
LoginController
@RestController
public class LoginController {
@Autowired
private UserService service;
@GetMapping("/hello")
public String hello(){
return "Hello Word!";
}
@GetMapping("/hello1")
public String hello1(){
return "Hello1 Word!";
}
@PostMapping("/login")
public RespBean loginUser(@RequestBody User user, HttpServletRequest request){
return service.login(user.getUsername(),user.getPassword(),request);
}
}
UserService
,运用的时 MVC 形式,所以只展现完结类:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtUtil jwtUtil;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
public RespBean login(String username, String password, HttpServletRequest request) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails==null || !passwordEncoder.matches(password,userDetails.getPassword())){
return RespBean.error("用户名或暗码过错!");
}
if (!userDetails.isEnabled()){
return RespBean.error("用户状况异常!");
}
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());
String jwt = jwtUtil.foundJWT(userDetails);
SecurityContextHolder.getContext().setAuthentication(token);
Map<String,String> msg = new HashMap<>();
msg.put("tokenHead",tokenHead);
msg.put("token", jwt);
return RespBean.success("登录成功!",msg);
}
}
7.1 完善 Swagger2Config 装备
因为 JWT 的参加,所以 Swagger2 的办法恳求也是需求带入 JWT 令牌,供给了 Security 的大局认证。
只展现了修正的部分。
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo()) // 装备 apiInfo
.select() // 选择那些途径和api会生成document
.apis(RequestHandlerSelectors.basePackage("com.cnda.controller")) // // 对哪些 api进行监控,RequestHandlerSelectors.basePackage 依据包扫描
.paths(PathSelectors.any()) // 对一切途径进行监控
.build()
// 增加和 Security 相关的装备。
.securityContexts(securityContexts())
.securitySchemes(securitySchemes());
}
// 以下办法相对于给 Swagger 增加了一个在 Security 的大局授权,而且以正则的方式设置了授权的恳求 url
/**
* securityContexts
* 恳求体内容
*/
private List<SecurityContext> securityContexts(){
List<SecurityContext> securityContexts = new ArrayList<>();
securityContexts.add(getContextByPath("/hello/.*"));
return securityContexts;
}
// 通过正则表达式来设置哪些途径
// 通过 Path 获取到对应的 SecurityContext
private SecurityContext getContextByPath(String pathRegex) {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex(pathRegex)) // 依照 String 的 matches 办法进行匹配
.build();
}
/**
* 装备默许的大局鉴权战略;其间回来的 SecurityReference 中,reference 即为 ApiKey 目标里面的name,保持共同才干敞开大局鉴权
* @return SecurityReference
*/
private List<SecurityReference> defaultAuth() {
List<SecurityReference> references = new ArrayList<>();
// scope 参数:
AuthorizationScope authorizationScope = new AuthorizationScope("global","accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
references.add(new SecurityReference("Authorization",authorizationScopes));
return references;
}
/**
* securitySchemes
* 安全体计划
*/
private List<SecurityScheme> securitySchemes(){
List<SecurityScheme> apiKeys = new ArrayList<>();
// 设置恳求头信息
apiKeys.add(new ApiKey("Authorization","Authorization","Header"));
return apiKeys;
}
修正的部分直接 CV 大法即可。
7.2 运转项目查看作用:
能够看到使用 Swagger2 的调试,回来 JWT Token 令牌成功!
{
"code": 200,
"message": "登录成功!",
"obj": {
"tokenHead": "Bearer",
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImlidCI6MTY3Nzk4NzIwNjgyMSwiZXhwIjoxNjc4NTkyMDA2fQ.p_GUqevx8gvCK2txxeEX-RQFm69yDCxCYNlZbeHgVIizSUDO6gaT3a2jGXvzXqofH2uxkQBgN4WfeSIlGydiNA"
}
}
将令牌设置到 Swagger2 中
这样之前的 /hello1
就能够恳求成功了:
阐明 Swagger2 设置 JWT 也成功了,每次发送恳求,头部都会带着 JWT 令牌。
总结
仍是对 Security 不太熟悉,Swagger2 的装备比较固定
JWT 首要也是两个点:
- JWT Token Utile 东西类,首要用于办理 JWT 令牌。
- JWT Token Filter JWT 阻拦器,这个就是 Security 和 JWT 的集成了,以及恳求发来的时分解析 JWT 从而完结免登录这一操作。