SpringBoot Security+JWT授权验证模块使用

简介

通过JWT每次请求带上token可以进行无状态登录,不必保存Session等。

创建

在IDEA中使用Spring Initializr创建一个Security项目,注意,创建之后maven是不可用的,还需要添加maven支持,模块右键,Add Framework Support,选择maven

依赖

添加JWT依赖,和一个web依赖包,毕竟要用来登录认证。

		<!--jwt-->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.1</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
准备

提前准备好一些实体类,和一些常量。
User实体类,实现了Spring SecurityUserDetails接口,其实没用到这些方法,不实现也可以。

@Entity
public class User implements UserDetails {
    private String username;
    private String password;

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

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

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

    @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 boolean isEnabled() {
        return true;
    }
}

一些常量,SECRET token签名用的,以及token过期时间等。

public class SecurityConstants {
    public static final String SECRET = "SecretKeyToGenJWTs";
    public static final long EXPIRATION_TIME = 864_000_000; // 10 days
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String HEADER_STRING = "Authorization";
    public static final String APPLICATION_JSON_VALUE = "application/json";

}
配置自定义的用户存储认证

这种方式更为灵活,更适合在生产环境使用。这种方式不在局限于存储环境。自定义的方式也很简单。只需要提供一个UserDetailService接口实现即可。
这里没有做任何校验可以在这里去获取数据库存储的用户进行校验,我为了测试方便,就认为验证通过了,并且获取到了密码,当然不是存储的明文密码,所以使用BCryptPasswordEncoder进行编码。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) {
        //做校验这里
        // TODO: 2020/12/23  
        return new User(username, new BCryptPasswordEncoder().encode("12345"), Collections.emptyList());
    }
}
Controller

Controller增加两个接口,/loginSecurity中放行,然后用携带的用户免密去我们自定义的JWTFilter中认证获取token返回给前端,前端拿到token之后就可以在每次请求时携带token进行访问,不会被拦截(这里/login为了方便查看日志,实际上SpringSecurity提供默认的登录页面)。

@RestController
@Slf4j
public class IndexController {

    @RequestMapping(value = "/login",method = RequestMethod.POST)
    public boolean login(@RequestBody String raw) {
        log.info("接口调用:{}",raw);
        return true;
    }

    @RequestMapping(value = "/auth")
    public String auth(@RequestBody User user) {
        log.info("User,name:{},value{}",user.getUsername(),user.getPassword());
        return "success";
    }
}

配置JWTFilter

首先配置JWTAuthenticationFilter认证Filter,我们创建的身份验证过滤器扩展了UsernamepasswordauthenticationFilter类。当我们向Spring Security添加新过滤器时,我们可以明确定义我们希望该过滤器的过滤器链中的位置,或者我们可以让框架自行删除它。通过扩展安全框架内提供的过滤器,Spring可以自动识别将其放在安全链中的最佳位置。

我们的自定义身份验证过滤器覆盖了基类的两种方法:
attemptAuthentication:解析用户的凭据并将它们发出到AuthenticationManager。
successfulAuthentication:成功登录时调用。我们使用此方法为此用户生成token。1

@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    /*解析用户使用manager进行认证*/
    @Override
    public Authentication attemptAuthentication(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws AuthenticationException {
        try {
            User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
            log.info("username:{},pwd:{}",user.getUsername(),user.getPassword());
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

    /*当成功时调用*/
    @Override
    protected void successfulAuthentication(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, javax.servlet.FilterChain chain, Authentication authResult) throws IOException, ServletException, ServletException {
        String token = Jwts.builder()
                .setSubject(((UserDetails) authResult.getPrincipal()).getUsername())//用户名
                .setExpiration(new Date(System.currentTimeMillis() +EXPIRATION_TIME))//过期时间
                .signWith(SignatureAlgorithm.HS256, SECRET)//签名
                .compact();
        log.info("token:{}", token);
        Map<String, String> res = new HashMap<>();
        res.put(HEADER_STRING, TOKEN_PREFIX + token);
        response.getWriter().println(new ObjectMapper().writeValueAsString(res));
    }
}

AuthenticationManager是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法authenticate(),该方法只接收一个代表认证请求的Authentication对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的Authentication对象进行返回。

Authentication authenticate(Authentication authentication) throws AuthenticationException;

Spring Security中,AuthenticationManager的默认实现是ProviderManager,而且它不直接自己处理认证请求,而是委托给其所配置的AuthenticationProvider列表,然后会依次使用每一个AuthenticationProvider进行认证,如果有一个AuthenticationProvider认证后的结果不为null,则表示该AuthenticationProvider已经认证成功,之后的AuthenticationProvider将不再继续认证。然后直接以该AuthenticationProvider的认证结果作为ProviderManager的认证结果。如果所有的AuthenticationProvider的认证结果都为null,则表示认证失败,将抛出一个ProviderNotFoundException
校验认证请求最常用的方法是根据请求的用户名加载对应的UserDetails,然后通过DaoAuthenticationProvider#additionalAuthenticationChecks比对UserDetails的密码与认证请求的密码是否一致,一致则表示认证通过。
Spring Security内部的DaoAuthenticationProvider就是使用的这种方式。其内部使用UserDetailsService来负责加载UserDetails。在认证成功以后会使用加载的UserDetails来封装要返回的Authentication对象,加载的UserDetails对象是包含用户权限等信息的。认证成功返回的Authentication对象将会保存在当前的SecurityContext中。一个具有注脚的文本。2

JWTAuthorizationFilter授权Filter配置。
这个类作用是授权

@Slf4j
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
    public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    /*接收token*/
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader(HEADER_STRING);
        if (null == header || !header.startsWith(TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(request);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        chain.doFilter(request,response);
    }

    /*解析token授权*/
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader(HEADER_STRING);
        if (null != token) {
            String user = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody()
                    .getSubject();
            if (null != user) {
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
            }
        }
        return null;

    }
}

我们通过扩展BasicAuthenticationFilter,用自定义实现将其替换,最重要部分是getAuthentication方法。此方法从Header读取token,然后使用JWT验证令牌。如果一切顺利,我们将用户设置为SecurityContext并允许请求进入下一个链。

整合Filter到SpringBoot

现在万事俱备,我们只需要将刚刚创建的Filter配置到SpringBoot中。

注意这里的注解,决定了是否启用Security,三个配置类,

  1. 分别配置了用户自定义认证以及加密工具;
  2. 资源放行;
  3. 权限配置。
@Configuration //开启配置
@EnableWebSecurity //开启Security配置
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;//自定义用户认证(校验用法是否合法)

    /*加密工具*/
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /*加密*/
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    /*放过资源,不经过安全校验*/
    @Override
    public void configure(WebSecurity web) {
        web
                .ignoring().antMatchers("/health");

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .csrf().disable()
                .authorizeRequests()//开始配置权限
                .antMatchers("/login")//放过他用于验证登录
                .permitAll()//任何人
                .anyRequest().authenticated()//所有请求都要经过授权
                .and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))//自定义Filter
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

config中权限配置比较多,这里全局放行了/health,权限配置放行了/login,还有一个/auth需要使用token才能访问。下面进行验证。
验证/health
在这里插入图片描述
验证/login
正确密码:
在这里插入图片描述
错误密码:
在这里插入图片描述
失败重定向了,这里没有配置失败重定向。
验证/auth
token
在这里插入图片描述
不带token
在这里插入图片描述
失败重定向。

总结

SpringBoot Security+JWT使用还是挺简单的,但是原理和配置有些复杂,还没有仔细研究,后面再研一下。


  1. 参考链接:Implementing JWT Authentication on Spring Boot APIs ↩︎

  2. 参考链接:spring-security权限控制详解 ↩︎

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot是一个基于Spring框架的快速开发Web应用程序的框架,Spring SecuritySpring框架的安全模块JWT是一种用于身份验证的开放标准。Vue是一种流行的JavaScript框架,用于构建用户界面。 结合这些技术,可以实现前后端分离的登录、权限管理和Token管理。具体步骤如下: 1. 在Spring Boot项目中导入Spring SecurityJWT的Maven依赖。 2. 配置Spring Security,包括创建用户、角色和权限等。 3. 创建一个JWT工具类,用于生成和解析Token。 4. 创建一个登录接口,接收用户名和密码,验证用户信息,生成Token并返回给前端。 5. 创建一个Token验证过滤器,用于验证请求中的Token是否有效。 6. 在Vue项目中使用Axios发送登录请求,获取Token并保存到本地存储中。 7. 在Vue项目中使用Vue Router和VueX进行路由和状态管理。 8. 创建一个路由守卫,用于验证用户是否登录和是否有权限访问某些页面。 9. 在需要进行身份验证的请求中添加Token。 下面是一个简单的示例代码,仅供参考: 后端代码: ```java // 配置Spring Security @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } } // 创建一个JWT工具类 public class JwtUtils { private static final String SECRET_KEY = "mySecretKey"; private static final long EXPIRATION_TIME = 86400000; // 24 hours public static String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return Jwts.builder() .setClaims(claims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); } public static String getUsernameFromToken(String token) { return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getSubject(); } public static boolean validateToken(String token, UserDetails userDetails) { String username = getUsernameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } private static boolean isTokenExpired(String token) { Date expiration = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getExpiration(); return expiration.before(new Date()); } } // 创建一个登录接口 @RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @PostMapping("/login") public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername()); String token = JwtUtils.generateToken(userDetails); return ResponseEntity.ok(new JwtAuthenticationResponse(token)); } } // 创建一个Token验证过滤器 public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtUtils jwtUtils; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); String username = jwtUtils.getUsernameFromToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtUtils.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } } // 创建一个自定义的AuthenticationEntryPoint @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); } } // 创建一个自定义的UserDetailsService @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("User not found with username: " + username); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), new ArrayList<>()); } } // 创建一个实体类User和一个接口UserRepository @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "username") private String username; @Column(name = "password") private String password; // getters and setters } @Repository public interface UserRepository extends JpaRepository<User, Long> { User findByUsername(String username); } ``` 前端代码: ```javascript // 在Vue项目中使用Axios发送登录请求 axios.post('/api/auth/login', { username: 'admin', password: 'password' }).then(response => { localStorage.setItem('token', response.data.token); }); // 在需要进行身份验证的请求中添加Token axios.get('/api/users', { headers: { Authorization: 'Bearer ' + localStorage.getItem('token') } }); // 创建一个路由守卫 router.beforeEach((to, from, next) => { const publicPages = ['/login', '/register']; const authRequired = !publicPages.includes(to.path); const loggedIn = localStorage.getItem('token'); if (authRequired && !loggedIn) { return next('/login'); } next(); }); // 使用VueX进行状态管理 const store = new Vuex.Store({ state: { isLoggedIn: !!localStorage.getItem('token') }, mutations: { login(state) { state.isLoggedIn = true; }, logout(state) { state.isLoggedIn = false; } }, actions: { login({ commit }) { return new Promise(resolve => { axios.post('/api/auth/login', { username: 'admin', password: 'password' }).then(response => { localStorage.setItem('token', response.data.token); commit('login'); resolve(); }); }); }, logout({ commit }) { return new Promise(resolve => { localStorage.removeItem('token'); commit('logout'); resolve(); }); } } }); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值