Spring Security + JWT 入门实战
##主要步骤
- 搭建基础的springboot工程,导入相关依赖
- 配置mysql,引用jpa
- 开启JPA支持
- 创建User实体,及controller,service,repository相关类
- 创建Jwt工具类,用于管理token相关的操作
- 创建JwtUser类,主要用于封装登录用户相关信息,例如用户名,密码,权限集合等,必须实现UserDetails 接口
- 创建JwtUserService 必须实现UserDetailsService,重写loadUserByUsername()方法,这样我们可以查询自己的数据库是否存在当前登录的用户名
- 创建拦截器,主要用于拦截用户登录信息,验证的事交给spring-security自己去做,验证成功会返回一个token,失败返回错误信息即可
- 用户验证成功过后会拿到token,下面的请求就需要携带这个token,后台需要一个新的拦截器进行权限验证
- 两个拦截器有了之后,只需要一个SecurityConfig将他们串联起来就行了
1. 搭建基础的springboot工程,导入相关依赖,项目整体结构和pom.xml 文件如下
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--springSecurity跟jwt的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--添加jpa支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--mysql依赖包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--通过lombok包,实体类中不需要再写set,get方法,只需要添加一个@Data注解即可-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
</dependencies>
2.配置mysql,引用jpa,application.properties文件如下
#端口设置
server.port=8088
#数据库连接
spring.datasource.url=jdbc:mysql://localhost:3306/db01?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
jpa配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
3. 开启JPA支持,启动项上加@EnableJpaRepositories注解即可
@SpringBootApplication
@EnableJpaRepositories// 加一个这个注解即可开启JPA支持
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
4.新增一张User表,及controller,service,repository相关类
@Entity
@Data // 注入该注解可以免去写set get方法
public class User {
@Id
@GeneratedValue
private Integer id;
private String username;
private String password;
private String role;
}
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
UserServiceInterface userServiceInterface;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@PostMapping
public User save(@RequestBody User parameter) {
User user = new User();
user.setUsername(parameter.getUsername());
user.setPassword(bCryptPasswordEncoder.encode(parameter.getPassword()));
if("admin".equals(parameter.getUsername())){
user.setRole("ADMIN");
}else{
user.setRole("USER");
}
return userServiceInterface.save(user);
}
@GetMapping
public User findByUsername(@RequestParam String username){
return userServiceInterface.findByUsername(username);
}
@GetMapping("/findAll")
@PreAuthorize("hasAnyAuthority('ADMIN')") //这一步很重要 拥有ADMIN权限的用户才能访问该请求
public List<User> findAll(){
return userServiceInterface.findAll();
}
}
/**
* 一定要加上 @Service 注解
*/
@Service
public class UserService implements UserServiceInterface {
@Autowired
UserRepository userRepository;
@Override
public User save(User user) {
return userRepository.save(user);
}
@Override
public User findByUsername(String username) {
return userRepository.findByUsername(username);
}
@Override
public List<User> findAll() {
return userRepository.findAll();
}
}
public interface UserServiceInterface {
User save(User user);
User findByUsername(String username);
List<User> findAll();
}
/**
* @Repository 必须加上
* 必须继承 extends JpaRepository<User,Long>
*/
@Repository
public interface UserRepository extends JpaRepository<User,Long> {
User save(User user);
User findByUsername(String username);
List<User> findAll();
}
5.创建Jwt工具类,用于管理token相关的操作
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* jwt 工具类 主要是生成token 检查token等相关方法
*/
public class JwtUtils {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
// TOKEN 过期时间
public static final long EXPIRATION = 1000 * 60 * 30; // 三十分钟
public static final String APP_SECRET_KEY = "secret";
private static final String ROLE_CLAIMS = "rol";
/**
* 生成token
*
* @param username
* @param role
* @return
*/
public static String createToken(String username, String role) {
Map<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
String token = Jwts
.builder()
.setSubject(username)
.setClaims(map)
.claim("username", username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS256, APP_SECRET_KEY).compact();
return token;
}
/**
* 获取当前登录用户用户名
*
* @param token
* @return
*/
public static String getUsername(String token) {
Claims claims = Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();
return claims.get("username").toString();
}
/**
* 获取当前登录用户角色
*
* @param token
* @return
*/
public static String getUserRole(String token) {
Claims claims = Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();
return claims.get("rol").toString();
}
/**
* 获解析token中的信息
*
* @param token
* @return
*/
public static Claims checkJWT(String token) {
try {
final Claims claims = Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();
return claims;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 检查token是否过期
*
* @param token
* @return
*/
public static boolean isExpiration(String token) {
Claims claims = Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();
return claims.getExpiration().before(new Date());
}
}
6.创建JwtUser类,主要用于封装登录用户相关信息,例如用户名,密码,权限集合等,必须实现UserDetails接口
public class JwtUser implements UserDetails {
private Integer id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public JwtUser() {
}
// 写一个能直接使用user创建jwtUser的构造器
public JwtUser(User user) {
id = user.getId();
username = user.getUsername();
password = user.getPassword();
authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
}
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public String getPassword() {
return password;
}
public String getUsername() {
return username;
}
public boolean isAccountNonExpired() {
return true;
}
public boolean isAccountNonLocked() {
return true;
}
public boolean isCredentialsNonExpired() {
return true;
}
public boolean isEnabled() {
return true;
}
}
7.创建JwtUserService 必须实现UserDetailsService,重写loadUserByUsername()方法
@Service
public class JwtUserService implements UserDetailsService {
@Autowired
UserService userService;
/**
* 根据前端传入的用户信息 去数据库查询是否存在该用户
* @param s
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = this.userService.findByUsername(s);
if (user != null) {
JwtUser jwtUser = new JwtUser(user);
return jwtUser;
} else {
try {
throw new ValidationException("该用户不存在");
} catch (ValidationException e) {
e.printStackTrace();
}
}
return null;
}
}
8.配置拦截器,主要用于拦截用户登录信息
/**
* 验证用户名密码正确后,生成一个token,并将token返回给客户端
* 该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法 ,
* attemptAuthentication:接收并解析用户凭证。
* successfulAuthentication:用户成功登录后,这个方法会被调用,我们在这个方法里生成token并返回。
*/
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
/**
* security拦截默认是以POST形式走/login请求,我们这边设置为走/token请求
* @param authenticationManager
*/
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/token");
}
/**
* 接收并解析用户凭证
* @param request
* @param response
* @return
* @throws AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 从输入流中获取到登录的信息
try {
User loginUser = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword())
);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
// 成功验证后调用的方法
// 如果验证成功,就生成token并返回
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
System.out.println("jwtUser:" + jwtUser.toString());
String role = "";
Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
for (GrantedAuthority authority : authorities) {
role = authority.getAuthority();
}
String token = JwtUtils.createToken(jwtUser.getUsername(), role);
// 返回创建成功的token 但是这里创建的token只是单纯的token
// 按照jwt的规定,最后请求的时候应该是 `Bearer token`
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
String tokenStr = JwtUtils.TOKEN_PREFIX + token;
response.setHeader("token", tokenStr);
}
// 失败 返回错误就行
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.getWriter().write("authentication failed, reason: " + failed.getMessage());
}
}
9.权限拦截器
假如admin登录成功后,携带token去请求其他接口时,该拦截器会判断权限是否正确
/**
* 登录成功之后走此类进行 鉴定 权限
*/
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@SneakyThrows
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String tokenHeader = request.getHeader(JwtUtils.TOKEN_HEADER);
// 如果请求头中没有Authorization信息则直接放行了
if (tokenHeader == null || !tokenHeader.startsWith(JwtUtils.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
super.doFilterInternal(request, response, chain);
}
// 这里从token中获取用户信息并新建一个token 就是上面说的设置认证信息
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) throws Exception {
String token = tokenHeader.replace(JwtUtils.TOKEN_PREFIX, "");
// 检测token是否过期 如果过期会自动抛出错误
JwtUtils.isExpiration(token);
String username = JwtUtils.getUsername(token);
String role = JwtUtils.getUserRole(token);
if (username != null) {
return new UsernamePasswordAuthenticationToken(username, null,
Collections.singleton(new SimpleGrantedAuthority(role))
);
}
return null;
}
}
10. 配置SecurityConfig文件
注意@EnableGlobalMethodSecurity这个注解,上面UserController中我们用到了
@EnableWebSecurity
// 只有加了@EnableGlobalMethodSecurity(prePostEnabled=true) 那么在上面使用的 @PreAuthorize(“hasAuthority(‘admin’)”)才会生效
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtUserService jwtUserService;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 这边 通过重写configure(),去数据库查询用户是否存在
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(jwtUserService).passwordEncoder(bCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
// 以/user 开头的请求 都需要进行验证
.antMatchers("/user/**")
.authenticated()
// 其他都放行了
.anyRequest().permitAll()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager())) // 用户登录拦截
.addFilter(new JWTAuthorizationFilter(authenticationManager())) // 权限拦截
// 不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
11.验证我们的代码
现在启动项目默认已经有security拦截,因为我们还没有用户信息,所以需要先把第十步当中的 .antMatchers("/user/**").authenticated() 注释掉才能调用添加用户的API,这边配置的意思的以/user开头的请求都需要进行认证拦截,重启项目后开始以下操作
1.创建一个admin用户,我们在代码中默认给admin的role是"ADMIN",拦截器会用到这边的role
2.同样步骤再创建一个普通的user用户,他的role是"USER"
12.用户和角色已经有了,第11步中注释的代码可以放开,重启我们的服务,现在开始security验证
1.首先我们用user用户进行登录,拿到token,注意请求地址和token返回的位置
2.请求UserController中的findByUsername()方法,根据用户名称查询,这边没有加@PreAuthorize(“hasAnyAuthority(‘ADMIN’)”)权限验证,header中携带token,如下图我们可以请求到想要的接口
3.我们用同样的token请求UserController中findAll()方法,注意UserController中这边添加权限验证了,我们无权访问才对,看下图,与期望值一致
4.现在我们换admin用户重复第一步和第三步操作,如下图所示,换用admin返回的token去请求findAll()方法时,成功获取到想要的数据。
请求成功
本篇文章参考文献:https://blog.csdn.net/zhangcongyi420/article/details/91348402?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param.
附上源码地址:https://gitee.com/mao_jiafeng/spring-security.git.