SpringBoot2+SpringSecurity整合JWT,前后端分离的API权限认证框架搭建手册

前言

之前又用到JWT,但是基本都是别人搭建,直接使用,有什么可以优化的,也不知道,所以还是要自己实践一遍,实践才能出真理。也看到很多整合的文章,有些说的细致但是版本太了,有些说的不够详细,而且细节也挺多的,纸上得来终觉浅,绝知此事要躬行,所以自己动手实践了一下。

本文应该有很多中叫法的,可以叫:

  • springboot + spring security + jwt 实现api权限控制
  • 基于SpringSecurity和JWT的用户访问认证和授权
  • 使用JWT和Spring Security保护REST API
  • SpringSecurity整合JWT
  • Spring Security Tutorial: REST Security with JWT
  • 使用JWT保护你的Spring Boot应用 - Spring Security实战

Why&What JWT?

在这里插入图片描述
使用 JWT 做权限验证,相比传统Session的优点是,Session 需要占用大量服务器内存,并且在多服务器时就会涉及到Session共享问题,对手机等移动端访问时就比较麻烦,因此前后端分离的项目很多都用JWT来做。

  • JWT无需存储在服务器(不使用Session/Cookie),不占用服务器资源(也就是Stateless无状态的),也就不存在多服务器共享Session的问题
  • 使用简单,用户在登录成功拿到 Token后,一般访问需要权限的请求时,在Header附上Token即可。

开源项目

代码已经上传GITHUB,查看源码更方便。

文章从以下部分来描述:

  • Maven 依赖
  • Starter 启动器
  • Application 配置
  • Entity 实体类
  • Config 配置类
  • Service&Controller 服务和控制器类
  • Result 运行效果

Maven依赖

主要是securityjwt两个包,其他的用springboot+web那些就可以了.

  • org.springframework.boot.spring-boot-starter-security
  • io.jsonwebtoken.jjwt
		<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
		<dependency>
		    <groupId>org.springframework.boot</groupId>
		    <artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.1</version>
		</dependency>

Starter启动类

没什么特别的

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

Application.yml

主要看jwt部分,都是动态参数,从配置读取是比较合理的,代码不用改动。

  • authentication.path,使用username+password去获取认证的URL路径,这里设置为/auth
  • header,JWT的头部,一般是Authorization开头"Bearer token"的格式
  • secret,加密的密钥,随便设置一个呗,在集群环境下,只要保证这个一致就行了,生成一些复杂点的即可。
server:
  port: 9999
  servlet:
      context-path: /security
tomcat:
    remote-ip-header: x-forward-for
    uri-encoding: UTF-8
    max-threads: 10
    background-processor-delay: 30
#here is the importance configs of JWT
jwt:
  route:
    authentication:
      path: /auth
  header: Authorization
  expiration: 604800
  secret: zhengkai.blog.csdn.net

Entity实体类

  • JwtRequest,请求封装,主要包含usernamepassword字段,前台发后台的时候发json,@RequestBody可以直接转换。
  • JwtResponse,相应封装,主要包含jwttoken字段,直接返回对象即可。
  • JwtUser,实现了UserDetails接口,JWT用户相关封装。
@Data
public class JwtRequest implements Serializable {
	private static final long serialVersionUID = 1L;
	private String username;
	private String password;
}
@Data
public class JwtResponse implements Serializable {
	private static final long serialVersionUID = 1L;
	private String jwttoken;
	public JwtResponse(String jwttoken) {
		this.jwttoken = jwttoken;
	}
}
@Data
public class JwtUser implements UserDetails {

	private static final long serialVersionUID = 1L;

	private final String id;
	private final String username;
	private final String password;
	private final Collection<? extends GrantedAuthority> authorities;
	private final boolean enabled;

	public JwtUser(
			String id,
			String username,
			String password, List<String> authorities,
			boolean enabled
			) {
		this.id = id;
		this.username = username;
		this.password = password;
		this.authorities = mapToGrantedAuthorities(authorities);
		this.enabled = enabled;
	}
	public JwtUser(
			String id,
			String username,
			String password, String authoritie,
			boolean enabled
			) {
		this.id = id;
		this.username = username;
		this.password = password;
		this.authorities = mapToGrantedAuthorities(authoritie);
		this.enabled = enabled;
	}
	private List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
        return authorities.stream()
                .map(authority -> new SimpleGrantedAuthority(authority))
                .collect(Collectors.toList());
    }
	private List<GrantedAuthority> mapToGrantedAuthorities(String authoritie) {
        return Arrays.asList(new SimpleGrantedAuthority(authoritie));
    }
	public String getId() {
		return id;
	}

	@Override
	public String getUsername() {
		return username;
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public String getPassword() {
		return password;
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return authorities;
	}

	@Override
	public boolean isEnabled() {
		return enabled;
	}

}

Config配置类

  • JwtTokenUtil,JWT工具类,生成/验证/是否过期token 。
  • WebSecurityConfig,Security配置类,启用URL过滤,设置PasswordEncoder密码加密类。
  • JwtRequestFilter,过滤JWT请求,验证"Bearer token"格式,校验Token是否正确
  • JwtAuthenticationEntryPoint,实现AuthenticationEntryPoint类,返回认证不通过的信息
@Component
public class JwtTokenUtil implements Serializable {
    private static final long serialVersionUID = -2550185165626007488L;
    public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
    
    @Value("${jwt.secret}")
    private String secret;
    
    //retrieve username from jwt token
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }
    //retrieve expiration date from jwt token
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }
    //for retrieveing any information from token we will need the secret key
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }
    //check if the token has expired
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }
    //generate token for user
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }
    //while creating the token -
//1. Define  claims of the token, like Issuer, Expiration, Subject, and the ID
//2. Sign the JWT using the HS512 algorithm and secret key.
//3. According to JWS Compact Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
//   compaction of the JWT to a URL-safe string
    private String doGenerateToken(Map<String, Object> claims, String subject) {
        return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
                .signWith(SignatureAlgorithm.HS512, secret).compact();
    }
    //validate token
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
	@Autowired
	private JwtUserDetailsService jwtUserDetailsService;
	@Autowired
	private JwtRequestFilter jwtRequestFilter;


	@Value("${jwt.header}")
	private String tokenHeader;

	@Value("${jwt.route.authentication.path}")
	private String authenticationPath;

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		// configure AuthenticationManager so that it knows from where to load
		// user for matching credentials
		// Use BCryptPasswordEncoder
		auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
	}
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	@Bean
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}
	@Override
	protected void configure(HttpSecurity httpSecurity) throws Exception {
		System.out.println("authenticationPath:"+authenticationPath);
		// We don't need CSRF for this example
		httpSecurity.csrf().disable()
		.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()
		.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
		// dont authenticate this particular request
		.and()
		.authorizeRequests()
		.antMatchers(authenticationPath).permitAll()
		.anyRequest().authenticated()

		.and()	
		.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

		// disable page caching
		httpSecurity
		.headers()
		.frameOptions().sameOrigin()  // required to set for H2 else H2 Console will be blank.
		.cacheControl();
	}
	@Override
	public void configure(WebSecurity web) throws Exception {
		// AuthenticationTokenFilter will ignore the below paths
		web
		.ignoring()
		.antMatchers(
				HttpMethod.POST,
				authenticationPath
				)

		// allow anonymous resource requests
		.and()
		.ignoring()
		.antMatchers(
				HttpMethod.GET,
				"/",
				"/*.html",
				"/favicon.ico",
				"/**/*.html",
				"/**/*.css",
				"/**/*.js"
				);
	}
}
import java.io.IOException;
import java.io.Serializable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
	private static final long serialVersionUID = 1L;

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException {
		response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
	}
}

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.softdev.system.demo.service.JwtUserDetailsService;

import io.jsonwebtoken.ExpiredJwtException;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {
	@Autowired
	private JwtUserDetailsService jwtUserDetailsService;
	
	@Autowired
	private JwtTokenUtil jwtTokenUtil;
	
	@Value("${jwt.route.authentication.path}")
	private String authenticationPath;
	
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws ServletException, IOException {
		final String requestTokenHeader = request.getHeader("Authorization");
		String username = null;
		String jwtToken = null;
		// JWT报文表头的格式是"Bearer token". 去除"Bearer ",直接获取token
		// only the Token
		if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
			jwtToken = requestTokenHeader.substring(7);
			try {
				username = jwtTokenUtil.getUsernameFromToken(jwtToken);
			} catch (IllegalArgumentException e) {
				System.out.println("Unable to get JWT Token");
			} catch (ExpiredJwtException e) {
				System.out.println("JWT Token has expired");
			}
		} else {
			logger.warn("JWT Token does not begin with Bearer String");
		}
		// Once we get the token validate it.
		if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
			UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
			// if token is valid configure Spring Security to manually set
			// authentication
			if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
				UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
						userDetails, null, userDetails.getAuthorities());
				usernamePasswordAuthenticationToken
				.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
				// After setting the Authentication in the context, we specify
				// that the current user is authenticated. So it passes the
				// Spring Security Configurations successfully.
				SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
			}
		}
		chain.doFilter(request, response);
	}
}

Service与Controller

  • JwtUserDetailsService,实现UserDetailsService,重写loadUserByUsername方法,返回随机生成的user,pass是密码,这里固定生成的,如果你自己需要定制查询user的方法,请改造这里。(如果你没用hutool这个这么好用的库,那么可以用其他方法代替随机值,也可以从数据库查询/缓存查询,都在这改造)
  • JwtAuthenticationController,包含登陆查看token的方法
import org.apache.commons.lang.StringUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import com.softdev.system.demo.entity.JwtUser;

import cn.hutool.core.util.RandomUtil;
/**
 * JwtUserDetailsService
 *	 	实现UserDetailsService,重写loadUserByUsername方法
 *  	返回随机生成的user,pass是密码,这里固定生成的
 *  	如果你自己需要定制查询user的方法,请改造这里
 * @author zhengkai.blog.csdn.net
 */
@Service
public class JwtUserDetailsService implements UserDetailsService{
	@Override
	public UserDetails loadUserByUsername(String username) {
		String pass = new BCryptPasswordEncoder().encode("pass");
		if (StringUtils.isNotEmpty(username)&&username.contains("user")) {
			return new JwtUser(RandomUtil.randomString(8), username,pass,"USER", true);
		} else {
			throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
		}
	}
}


import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.softdev.system.demo.config.JwtTokenUtil;
import com.softdev.system.demo.entity.JwtRequest;
import com.softdev.system.demo.entity.JwtResponse;
import com.softdev.system.demo.entity.JwtUser;
import com.softdev.system.demo.service.JwtUserDetailsService;

/**
 * JwtAuthenticationController
 * 	包含登陆和查看token的方法
 * @author zhengkai.blog.csdn.net
 */
@RestController
@CrossOrigin
public class JwtAuthenticationController {
	@Autowired
	private AuthenticationManager authenticationManager;
	@Autowired
	private JwtTokenUtil jwtTokenUtil;
	@Autowired
	private JwtUserDetailsService userDetailsService;
	
	@Value("${jwt.header}")
	private String tokenHeader;

	@PostMapping("${jwt.route.authentication.path}")
	public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception {
		System.out.println("username:"+authenticationRequest.getUsername()+",password:"+authenticationRequest.getPassword());
		authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
		final UserDetails userDetails = userDetailsService
				.loadUserByUsername(authenticationRequest.getUsername());
		final String token = jwtTokenUtil.generateToken(userDetails);
		return ResponseEntity.ok(new JwtResponse(token));
	}
	
	private void authenticate(String username, String password) throws Exception {
		try {
			authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
		} catch (DisabledException e) {
			throw new Exception("USER_DISABLED", e);
		} catch (BadCredentialsException e) {
			throw new Exception("INVALID_CREDENTIALS", e);
		}
	}

	@GetMapping("/token")
	public JwtUser getAuthenticatedUser(HttpServletRequest request) {
		String token = request.getHeader(tokenHeader).substring(7);
		String username = jwtTokenUtil.getUsernameFromToken(token);
		JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username);
		return user;
	}

}

Result运行效果

  1. 授权接口

localhost:9999/security/auth

在这里插入图片描述
请求数据json:

{
	"username":"users",
	"password":"pass"
}

响应数据:

{
    "jwttoken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VycyIsImV4cCI6MTU2MzUzNTU1NiwiaWF0IjoxNTYzNTE3NTU2fQ.rpK2URUu6e_JdxQR0v6ClFz1O_-w4SRDlBqZKd2FYpl7WIjczlopFvAl7yShwyrudPhLCt8hdgqNzO4Wqu71Dw"
}
  1. token信息接口

localhost:9999/security/token

在这里插入图片描述
请求头(注意是Header不是Body,格式Bearer+空格+Token):

Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VycyIsImV4cCI6MTU2MzUzNTU1NiwiaWF0IjoxNTYzNTE3NTU2fQ.rpK2URUu6e_JdxQR0v6ClFz1O_-w4SRDlBqZKd2FYpl7WIjczlopFvAl7yShwyrudPhLCt8hdgqNzO4Wqu71Dw

响应数据:

{
    "accountNonExpired": true,
    "accountNonLocked": true,
    "authorities": [
        {
            "authority": "USER"
        }
    ],
    "credentialsNonExpired": true,
    "enabled": true,
    "id": "vmw553ro",
    "password": "$2a$10$W/cPKmgwZv4gVMKO4pvUsOr9fusTTOwiHu1QSSlMICB42hU.AjpAO",
    "username": "users"
}
发布了293 篇原创文章 · 获赞 401 · 访问量 111万+

猜你喜欢

转载自blog.csdn.net/moshowgame/article/details/96476554