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支持publicclassApplication{publicstaticvoidmain(String[] args){
SpringApplication.run(Application.class, args);}}
4.新增一张User表,及controller,service,repository相关类
@Entity
@Data// 注入该注解可以免去写set get方法publicclassUser{
@Id
@GeneratedValueprivate Integer id;private String username;private String password;private String role;}
@RequestMapping("/user")
@RestControllerpublicclassUserController{
@Autowired
UserServiceInterface userServiceInterface;
@Autowiredprivate BCryptPasswordEncoder bCryptPasswordEncoder;
@PostMappingpublic Usersave(@RequestBody User parameter){
User user=newUser();
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);}
@GetMappingpublic UserfindByUsername(@RequestParam String username){return userServiceInterface.findByUsername(username);}
@GetMapping("/findAll")
@PreAuthorize("hasAnyAuthority('ADMIN')")//这一步很重要 拥有ADMIN权限的用户才能访问该请求public List<User>findAll(){return userServiceInterface.findAll();}}
/**
* 一定要加上 @Service 注解
*/
@ServicepublicclassUserServiceimplementsUserServiceInterface{
@Autowired
UserRepository userRepository;
@Overridepublic Usersave(User user){return userRepository.save(user);}
@Overridepublic UserfindByUsername(String username){return userRepository.findByUsername(username);}
@Overridepublic List<User>findAll(){return userRepository.findAll();}}
publicinterfaceUserServiceInterface{
Usersave(User user);
UserfindByUsername(String username);
List<User>findAll();}
/**
* @Repository 必须加上
* 必须继承 extends JpaRepository<User,Long>
*/
@RepositorypublicinterfaceUserRepositoryextendsJpaRepository<User,Long>{
Usersave(User user);
UserfindByUsername(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等相关方法
*/publicclassJwtUtils{publicstatic final StringTOKEN_HEADER="Authorization";publicstatic final StringTOKEN_PREFIX="Bearer ";// TOKEN 过期时间publicstatic final longEXPIRATION=1000*60*30;// 三十分钟publicstatic final StringAPP_SECRET_KEY="secret";privatestatic final StringROLE_CLAIMS="rol";/**
* 生成token
*
* @param username
* @param role
* @return
*/publicstatic StringcreateToken(String username, String role){
Map<String, Object> map=newHashMap<>();
map.put(ROLE_CLAIMS, role);
String token= Jwts.builder().setSubject(username).setClaims(map).claim("username", username).setIssuedAt(newDate()).setExpiration(newDate(System.currentTimeMillis()+EXPIRATION)).signWith(SignatureAlgorithm.HS256,APP_SECRET_KEY).compact();return token;}/**
* 获取当前登录用户用户名
*
* @param token
* @return
*/publicstatic StringgetUsername(String token){
Claims claims= Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();return claims.get("username").toString();}/**
* 获取当前登录用户角色
*
* @param token
* @return
*/publicstatic StringgetUserRole(String token){
Claims claims= Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();return claims.get("rol").toString();}/**
* 获解析token中的信息
*
* @param token
* @return
*/publicstatic ClaimscheckJWT(String token){try{
final Claims claims= Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();return claims;}catch(Exception e){
e.printStackTrace();returnnull;}}/**
* 检查token是否过期
*
* @param token
* @return
*/publicstatic booleanisExpiration(String token){
Claims claims= Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();return claims.getExpiration().before(newDate());}}
6.创建JwtUser类,主要用于封装登录用户相关信息,例如用户名,密码,权限集合等,必须实现UserDetails接口
publicclassJwtUserimplementsUserDetails{private Integer id;private String username;private String password;private Collection<?extendsGrantedAuthority> authorities;publicJwtUser(){}// 写一个能直接使用user创建jwtUser的构造器publicJwtUser(User user){
id= user.getId();
username= user.getUsername();
password= user.getPassword();
authorities= Collections.singleton(newSimpleGrantedAuthority(user.getRole()));}public Collection<?extendsGrantedAuthority>getAuthorities(){return authorities;}public StringgetPassword(){return password;}public StringgetUsername(){return username;}public booleanisAccountNonExpired(){returntrue;}public booleanisAccountNonLocked(){returntrue;}public booleanisCredentialsNonExpired(){returntrue;}public booleanisEnabled(){returntrue;}}
7.创建JwtUserService 必须实现UserDetailsService,重写loadUserByUsername()方法
@ServicepublicclassJwtUserServiceimplementsUserDetailsService{
@Autowired
UserService userService;/**
* 根据前端传入的用户信息 去数据库查询是否存在该用户
* @param s
* @return
* @throws UsernameNotFoundException
*/
@Overridepublic UserDetailsloadUserByUsername(String s) throws UsernameNotFoundException{
User user=this.userService.findByUsername(s);if(user!=null){
JwtUser jwtUser=newJwtUser(user);return jwtUser;}else{try{thrownewValidationException("该用户不存在");}catch(ValidationException e){
e.printStackTrace();}}returnnull;}}
8.配置拦截器,主要用于拦截用户登录信息
/**
* 验证用户名密码正确后,生成一个token,并将token返回给客户端
* 该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法 ,
* attemptAuthentication:接收并解析用户凭证。
* successfulAuthentication:用户成功登录后,这个方法会被调用,我们在这个方法里生成token并返回。
*/publicclassJWTAuthenticationFilterextendsUsernamePasswordAuthenticationFilter{private AuthenticationManager authenticationManager;/**
* security拦截默认是以POST形式走/login请求,我们这边设置为走/token请求
* @param authenticationManager
*/publicJWTAuthenticationFilter(AuthenticationManager authenticationManager){this.authenticationManager= authenticationManager;super.setFilterProcessesUrl("/token");}/**
* 接收并解析用户凭证
* @param request
* @param response
* @return
* @throws AuthenticationException
*/
@Overridepublic AuthenticationattemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException{// 从输入流中获取到登录的信息try{
User loginUser=newObjectMapper().readValue(request.getInputStream(), User.class);return authenticationManager.authenticate(newUsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword()));}catch(IOException e){
e.printStackTrace();returnnull;}}// 成功验证后调用的方法// 如果验证成功,就生成token并返回
@OverrideprotectedvoidsuccessfulAuthentication(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<?extendsGrantedAuthority> 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);}// 失败 返回错误就行
@OverrideprotectedvoidunsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException{
response.getWriter().write("authentication failed, reason: "+ failed.getMessage());}}
9.权限拦截器
假如admin登录成功后,携带token去请求其他接口时,该拦截器会判断权限是否正确
/**
* 登录成功之后走此类进行 鉴定 权限
*/publicclassJWTAuthorizationFilterextendsBasicAuthenticationFilter{publicJWTAuthorizationFilter(AuthenticationManager authenticationManager){super(authenticationManager);}
@SneakyThrows
@OverrideprotectedvoiddoFilterInternal(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