springboot前后端分离项目集成Security框架和JWT完成用户校验

前言

Java流行的安全框架有两种Apache Shiro和Spring Security,其中Shiro对于前后端分离项目不是很友好,最终选用了Spring Security。SpringBoot提供了官方的spring-boot-starter-security,能够方便的集成到SpringBoot项目中,但是企业级的使用上,还是需要稍微改造下,本文实现了如下功能:

  • 匿名用户访问无权限资源时的异常处理
  • 登录用户是否有权限访问资源
  • 集成JWT实现登陆授权访问
  • token过期处理
  • 自定义用户登陆逻辑

SpringSecurity简介

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于sprin的应用程序的标准。

Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求

Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理

1. 引入必要依赖

<!--spring security-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- redis 缓存操作 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

spring-boot-starter-security用于集成security, spring-boot-starter-data-redis用于实现redis缓存

2. 实现AuthenticationEntryPoint

实现AuthenticationEntryPoint,控制用户无权限时的操作

/**
* 认证失败处理类 返回未授权
*
* @author fanglei
* @Date 2023年7月31日17:54:39
*/
@Component
    public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
    private static final long serialVersionUID = -8970718410437077606L;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
    throws IOException
    {
        int code = HttpStatus.UNAUTHORIZED;
        String msg = StringUtils.format("请求访问:{},认证失败,请联系管理员", request.getRequestURI());
        ServletUtils.renderString(response, JSON.toJSONString(ApiResult.error(code, msg)));
    }
}

3. 实现UserDetailsService

实现了UserDetailsService,判断用户是否存在,完成用户自定义登陆

/**
 * 用户验证处理
 *
 * @author fanglei
 */
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService
{
     private static final String isEnable ="1";

    @Autowired
    private SysUserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        SysUser user = userService.getBaseMapper().selectOne(new QueryWrapper<SysUser>().eq("user_name", username));
        if (StringUtils.isNull(user))
        {
            log.info("登录用户:{} 不存在.", username);
            throw new BaseException("登录用户:" + username + " 不存在");
        }
        else if (isEnable.equals(user.getIsDel()))
        {
            log.info("登录用户:{} 已被删除.", username);
            throw new BaseException("对不起,您的账号:" + username + " 已被删除");
        }
        else if (isEnable.equals(user.getStatus()))
        {
            log.info("登录用户:{} 已被停用.", username);
            throw new BaseException("对不起,您的账号:" + username + " 已停用");
        }
        return new LoginUser(user.getUserId(), user,null);
    }

}
/**
 * @author 方磊
 */
@Data
public class LoginUser implements UserDetails {
    private static final long serialVersionUID = 1L;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 租户id
     */
    private Long tenantId;

    /**
     * 用户唯一标识
     */
    private String token;

    /**
     * 登录时间
     */
    private Long loginTime;

    /**
     * 过期时间
     */
    private Long expireTime;

    /**
     * 登录IP地址
     */
    private String ipaddress;

    /**
     * 登录地点
     */
    private String loginLocation;

    /**
     * 权限列表
     */
    private Set<String> permissions;

    /**
     * 用户信息
     */
    private SysUser user;

    public LoginUser(SysUser user, Set<String> permissions)
    {
        this.user = user;
        this.permissions = permissions;
    }

    public LoginUser(Long userId, SysUser user, Set<String> permissions)
    {
        this.userId = userId;
        this.user = user;
        this.permissions = permissions;
    }

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

    @JSONField(serialize = false)
    @Override
    public String getPassword()
    {
        return user.getPassword();
    }

    @Override
    public String getUsername()
    {
        return user.getUserName();
    }

    /**
     * 账户是否未过期,过期无法验证
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonExpired()
    {
        return true;
    }

    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonLocked()
    {
        return true;
    }

    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isCredentialsNonExpired()
    {
        return true;
    }

    /**
     * 是否可用 ,禁用的用户不能身份验证
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isEnabled()
    {
        return true;
    }
}

4. 退出登陆处理

退出登陆:删除用户缓存纪录并且发送给前端

/**
 * 自定义退出处理类 返回成功
 *
 * @author fanglei
 */
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
    @Autowired
    private TokenService tokenService;

    /**
     * 退出处理
     *
     * @return
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException
    {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser))
        {
            String userName = loginUser.getUsername();
            // 删除用户缓存记录
            tokenService.delLoginUser(loginUser.getToken());
        }
        ServletUtils.renderString(response, JSON.toJSONString(ApiResult.success("退出成功")));
    }
}

5. 配置security配置

/**
 * spring security配置
 *
 * @author fanglei
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;

    /**
     * 允许匿名访问的地址
     */
    @Autowired
    private PermitAllUrlProperties permitAllUrl;

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        // 注解标记允许匿名访问的url
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
        permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());

        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 禁用HTTP响应标头
                .headers().cacheControl().disable().and()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/login", "/register", "/captchaImage").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        // 添加Logout filter
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

6. 生成token

    /**
     * 创建令牌
     *
     * @param loginUser 用户信息
     * @return 令牌
     */
    public String createToken(LoginUser loginUser)
    {
        String token = IdUtils.fastUUID();
        loginUser.setToken(token);
        setUserAgent(loginUser);
        refreshToken(loginUser);

        Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_USER_KEY, token);
        return createToken(claims);
    }

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String createToken(Map<String, Object> claims)
    {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
        return token;
    }

    /**
     * 验证令牌有效期,相差不足20分钟,自动刷新缓存
     *
     * @param loginUser
     * @return 令牌
     */
    public void verifyToken(LoginUser loginUser)
    {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
        {
            refreshToken(loginUser);
        }
    }

    /**
     * 刷新令牌有效期
     *
     * @param loginUser 登录信息
     */
    public void refreshToken(LoginUser loginUser)
    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

7. 实现用户登录接口

/**
 * @author 方磊
 */
@RequestMapping("/api/v1")
@RestController
public class LoginController {

    @Autowired
    private TokenService tokenService;

    @Autowired
    private SysUserService userService;
    
    @GetMapping("/login")
    public ApiResult login(String userName , String password){
        LoginUser loginUser = sysService.queryUserByUserNmae(userName);
        retrun ApiResult.success(tokenService.createToken(loginUser));
    }
    
}

8. 总结

自此security简单集成完成 基础项目可简单使用 可以使用token进行验证 权限模块完善中…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
JWT(JSON Web Token)是一种基于 JSON 的安全令牌,它可以在客户端和服务器之间安全传输信息。在前后端分离项目中,使用 JWT 进行用户认证可以方便地处理用户登录和权限控制。 以下是使用 Spring BootJWT 实现前后端分离用户认证示例: 1. 添加依赖 在 pom.xml 中添加以下依赖: ```xml <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> ``` 2. 创建 JWT 工具类 创建一个 JWT 工具类,用于生成和验证 JWT。以下是一个简单的实现: ```java import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; import java.util.Map; @Component public class JwtUtils { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; public String generateToken(String username) { Map<String, Object> claims = new HashMap<>(); claims.put("sub", username); claims.put("iat", new Date()); return Jwts.builder() .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public String getUsernameFromToken(String token) { try { Claims claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); return claims.getSubject(); } catch (Exception e) { return null; } } public boolean validateToken(String token, String username) { String tokenUsername = getUsernameFromToken(token); return tokenUsername != null && tokenUsername.equals(username); } } ``` 3. 创建登录接口 创建一个登录接口,用于验证用户用户名和密码,并返回 JWT: ```java @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = jwtUtils.generateToken(authentication.getName()); return ResponseEntity.ok(new JwtResponse(jwt)); } ``` 4. 创建受保护的接口 创建一个受保护的接口,需要用户在请求头中传递 JWT。在该接口中,首先验证 JWT 是否有效,然后根据用户角色返回相应的数据: ```java @GetMapping("/data") @PreAuthorize("hasAnyRole('USER', 'ADMIN')") public ResponseEntity<?> getData() { String username = SecurityContextHolder.getContext().getAuthentication().getName(); if (jwtUtils.validateToken(jwt, username)) { // 返回数据 } else { throw new BadCredentialsException("Invalid JWT"); } } ``` 5. 配置 JWT 相关属性 在 application.properties 中配置 JWT 相关属性: ``` jwt.secret=mySecret jwt.expiration=86400000 # 1 day in milliseconds ``` 以上就是使用 Spring BootJWT 实现前后端分离用户认证的示例。需要注意的是,JWT 本身并没有提供加密功能,因此需要使用一个密钥来保证 JWT 的安全性。在示例中,密钥存储在配置文件中,实际生产环境中应该使用更加安全的方式来存储密钥。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

搬砖小方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值