对于《mall商城》中“mall整合SpringSecurity和JWT实现认证和授权”的理解

分析项目

mall-tiny-04

流转流程

1、访问登录接口

访问接口/admin/login来登录,由于该项目添加了SpringSecurity配置,所以我们来看下com.macro.mall.tiny.config.SecurityConfig,里面配置了对/admin/login接口是不需要认证,如下:
在这里插入图片描述

并且在com.macro.mall.tiny.controller.UmsAdminController》login方法上也没有添加@PreAuthorize,所以也不需要权限限制,既然认证授权都不需要,那就直接执行该方法即可

2、获取token返回前端

在上述的login方法中,我们可以看到以下代码:

String token = adminService.login(umsAdminLoginParam.getUsername(), umsAdminLoginParam.getPassword());

我们先来看该login方法中的第一行代码,如下:

UserDetails userDetails = userDetailsService.loadUserByUsername(username);

由于在com.macro.mall.tiny.config.SecurityConfig中重新定义了一个UserDetailsService,这就是上述userDetailsService对应的Bean

public UserDetailsService userDetailsService() {
    return new UserDetailsService() {
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            UmsAdmin admin = adminService.getAdminByUsername(username);
            if (admin != null) {
                List<UmsPermission> permissionList = adminService.getPermissionList(admin.getId());
                // 取自AuthenticationException
                return new AdminUserDetails(admin,permissionList);
            }
            // 取自AuthenticationException
            throw new UsernameNotFoundException("用户名或密码错误");
        }
    };
}

“mall作者”中是lambda表达式写法,这上面是匿名内部类写法,因此userDetailsService.loadUserByUsername(username)中调用的就是上面匿名内部类中的写法,我们来看匿名内部类中的第一行代码UmsAdmin admin = adminService.getAdminByUsername(username);,其实这行代码的作用就是根据用户名获取用户详细信息对象,当然我们还可以看getAdminByUsername()方法的具体实现,大家可能对example.createCriteria().andUsernameEqualTo(username);有所困惑,其实该代码的含义就是为sql语句拼接做准备,大家可以看一下UmsAdminMapper.xmlid=selectByExample的sql语句,其实<include refid="Example_Where_Clause" />作用就是拼接where username = "XXX",现在我们就从数据库中获取到用户详细信息了

之后根据用户id使用List<UmsPermission> permissionList = adminService.getPermissionList(admin.getId());从数据库中取出了用户的权限信息

最后通过return new AdminUserDetails(admin,permissionList);返回了用户信息和权限信息

当然AdminUserDetails实现了UserDetails,所以UserDetails userDetails = userDetailsService.loadUserByUsername(username);中才能使用UserDetails 来接收loadUserByUsername方法的返回值

我们继续回到com.macro.mall.tiny.service.impl.UmsAdminServiceImpl》login方法,现在我们已经获得了userDetails对象,里面有用户信息和权限信息,之后我们通过以下代码来验证密码是否正确,如下:

if (!passwordEncoder.matches(password, userDetails.getPassword())) {
    throw new BadCredentialsException("密码不正确");
}

之后把权限赋值给了用户,但是我感觉这个授权没啥用,毕竟直接把token返回了,权限也就在过滤器链中有效,如下:

UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);

之后通过工具类获取了token,如下:

token = jwtTokenUtil.generateToken(userDetails);

之后com.macro.mall.tiny.controller.UmsAdminController》login方法中就把token放在了Map集合中返回给前端

3、获取品牌列表

调用/brand/listAll接口,由于SpringSecurity的存在,所以一定会经过com.macro.mall.tiny.config.SecurityConfig,里面的configure方法会拦截该请求,然后交给httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);中的JwtAuthenticationTokenFilter对象处理,我们来看JwtAuthenticationTokenFilter的内部信息,里面有一个doFilterInternal方法,我们来具体看一下com.macro.mall.tiny.component.JwtAuthenticationTokenFilter》doFilterInternal方法的详细代码,如下:
第一行是String authHeader = request.getHeader(this.tokenHeader);,作用是获取header中的token,由于token以Bearer开头,所以就有了下述两条语句,如下:

if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
	String authToken = authHeader.substring(this.tokenHead.length());
	……
}

之后我们从token中获取username,如下:

String username = jwtTokenUtil.getUserNameFromToken(authToken);

getUserNameFromToken方法是用来获取username的,如果token有误或者token过期都会导致username是null,如果username是null,那么SecurityContextHolder.getContext().getAuthentication()的值就是null,那么在访问接口的时候就会抛出AuthenticationException异常,然后根据以下代码可以知道抛出异常的时候会交给restAuthenticationEntryPoint对象,如下:

httpSecurity.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);

大家可以看com.macro.mall.tiny.component.RestAuthenticationEntryPoint类里面的commence方法就会返回值给前端

我们可以继续回到com.macro.mall.tiny.component.JwtAuthenticationTokenFilter类,假设已经通过String username = jwtTokenUtil.getUserNameFromToken(authToken);代码取到了username,此时if (username != null && SecurityContextHolder.getContext().getAuthentication() == null)中的条件满足要求,通过UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);代码可以获取到userDetails 其中的loadUserByUsername方法调用的还是com.macro.mall.tiny.config.SecurityConfig#userDetailsService中的loadUserByUsername方法,上面已经分析过了,这里不再分析,之后又调用了if (jwtTokenUtil.validateToken(authToken, userDetails)),感觉这个调用没啥用,因为if中内容的作用是验证username是否正确和token是否过期,首先username来自于token,这肯定是一样的,另外token如果过期,那么username是null,然后就没法到这个if判断,所以我说这个判断很鸡肋,不知道你是否担心UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);获取的userDetails是null,其实不会的,因为如果无法获取到用户对象,早已经抛出了异常的,之后就是授权操作了,如果有相应的权限才可以调用相应的方法

如果用户有相应的权限值,那就可以通过@PreAuthorize("hasAuthority('pms:brand:read')")中的权限鉴定,如果无法通过权限鉴定,将会抛出AccessDeniedException异常,然后我们在com.macro.mall.tiny.config.SecurityConfig》configure中定义了如下以下处理器:

httpSecurity.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);

本次由restfulAccessDeniedHandler对象处理该异常,你也可以看到它会将某些内容返回到前端

4、在没有登录的情况下,无法访问没有权限注解的方法

估计是SecurityContextHolder.getContext().getAuthentication()为null,在调用到方法之前被SpringSecurity发现根本没有经过认证,所以触发了AuthenticationException异常,然后就根据com.macro.mall.tiny.component.RestAuthenticationEntryPoint》commence方法中的返回结果给前端做了返回,不过这也只是猜测,但是验证结果是未登录无法访问接口

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,这是一个很常见的问题。首先,你需要在Spring Boot项目添加依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> ``` 然后,你需要创建一个`JwtTokenFilter`类,用于在每个请求验证JWT令牌: ```java public class JwtTokenFilter extends OncePerRequestFilter { private final JwtTokenProvider tokenProvider; public JwtTokenFilter(JwtTokenProvider tokenProvider) { this.tokenProvider = tokenProvider; } @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { String token = tokenProvider.resolveToken(httpServletRequest); try { if (token != null && tokenProvider.validateToken(token)) { Authentication auth = tokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); } } catch (JwtException e) { SecurityContextHolder.clearContext(); httpServletResponse.sendError(HttpStatus.BAD_REQUEST.value(), e.getMessage()); return; } filterChain.doFilter(httpServletRequest, httpServletResponse); } } ``` 接下来,你需要创建一个`JwtTokenProvider`类,用于创建和验证JWT令牌: ```java @Component public class JwtTokenProvider { private static final String SECRET_KEY = "secret-key"; private static final long EXPIRATION_TIME = 864_000_000; // 10 days private final UserDetailsService userDetailsService; public JwtTokenProvider(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } public String createToken(Authentication authentication) { UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal(); Date now = new Date(); Date expiryDate = new Date(now.getTime() + EXPIRATION_TIME); return Jwts.builder() .setSubject(user.getUsername()) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); } public Authentication getAuthentication(String token) { UserDetails userDetails = userDetailsService.loadUserByUsername(getUsername(token)); return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } public String getUsername(String token) { return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getSubject(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { throw new JwtAuthenticationException("Expired or invalid JWT token"); } } public String resolveToken(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } ``` 最后,你需要修改`WebSecurityConfig`类,以使用JWT令牌进行身份验证: ```java @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final JwtTokenProvider tokenProvider; public WebSecurityConfig(JwtTokenProvider tokenProvider) { this.tokenProvider = tokenProvider; } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() .and() .apply(new JwtConfigurer(tokenProvider)); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } } ``` 以上就是整合Spring SecurityJWT进行身份验证的基本步骤。当然,具体实现还需要根据你的业务需求进行调整。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值