Spring Security + JWT 入门实战

Spring Security + JWT 入门实战

##主要步骤

  1. 搭建基础的springboot工程,导入相关依赖
  2. 配置mysql,引用jpa
  3. 开启JPA支持
  4. 创建User实体,及controller,service,repository相关类
  5. 创建Jwt工具类,用于管理token相关的操作
  6. 创建JwtUser类,主要用于封装登录用户相关信息,例如用户名,密码,权限集合等,必须实现UserDetails 接口
  7. 创建JwtUserService 必须实现UserDetailsService,重写loadUserByUsername()方法,这样我们可以查询自己的数据库是否存在当前登录的用户名
  8. 创建拦截器,主要用于拦截用户登录信息,验证的事交给spring-security自己去做,验证成功会返回一个token,失败返回错误信息即可
  9. 用户验证成功过后会拿到token,下面的请求就需要携带这个token,后台需要一个新的拦截器进行权限验证
  10. 两个拦截器有了之后,只需要一个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支持,启动项上加@EnableDiscoveryClient注解即可

@SpringBootApplication
@EnableDiscoveryClient // 加一个这个注解即可开启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.
如有不对的地方欢迎讨论 QQ:770850769

猜你喜欢

转载自blog.csdn.net/weixin_45452416/article/details/109528425