0


springboot 2.7整合spring security 5.7整合jwt实现用户登录注册与鉴权全记录

前言

关于系统最终想实现的功能:使用token来实现登录校验,用户登录后拿到token,然后将token放入httpHeader,之后每次接口请求都携带token,验证成功才可进行正常访问流程

储备知识

token与 jwt (JSON Web Token)介绍

JWT 的原理

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

{
  "姓名": "张三",
  "角色": "管理员",
  "到期时间": "2018年7月1日0点0分"
}

JWT 的数据结构

它是一个很长的字符串,中间用点(

.

)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。

JWT 的三个部分依次如下。

Header(头部)

Payload(负载)

Signature(签名)

Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

{
  "alg": "HS256",
  "typ": "JWT"
}

上面代码中,

alg

属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);

typ

属性表示这个令牌(token)的类型(type),JWT 令牌统一写为

JWT

最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。

Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Signature

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

 HMACSHA256(  base64UrlEncode(header) + "." +  base64UrlEncode(payload),  secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(

.

)分隔,就可以返回给用户。

JWT 工具类

有很多常见的工具类,我这边用的是这个

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

spring security简介

介绍的文章一抓一大把,这边主要说一下他的几个核心东西

用户认证(Authentication)

是验证您的身份的凭据(例如用户名/用户ID和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。

用户授权(Authorization)

发生在 **Authentication(认证)**之后。授权嘛,光看意思大家应该就明白,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。

过滤器链

核心组件

AuthenticationManager

SecurityContextHolder

PasswordEncoder

UserDetails

UserDetailsService

BasicAuthenticationFilter

AuthenticationEntryPoint

登录流程图

集成流程

集成spring security

maven依赖

springboot版本 2.7.3 springsecurity 版本 5.7.3

<!-- spring-boot -->
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.3</version>
        <relativePath/>
</parent>

<dependencies>
<!-- spring security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

WebSecurityConfig 配置

注意,springsecurity 5.7之后配置方式已经优化,无需再使用继承式的配置,直接bean方式配置即可

@EnableWebSecurity
@EnableConfigurationProperties(AuthProperties.class)
public class WebSecurityConfig {
    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private AuthProperties authProperties;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private JWTService jwtService;
    @Autowired
    private CacheManager cacheManager;

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                // 基于 token,不需要 csrf
                .csrf().disable()
                // 基于 token,不需要 session
                  .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 下面开始设置权限
                .authorizeRequests(authorize -> authorize
                        .antMatchers(authProperties.getPermitStatic().toArray(new String[0])).permitAll()
                        .antMatchers(authProperties.getPermitMethod().toArray(new String[0])).permitAll()
                        // 其他地址的访问均需验证权限
                        .anyRequest().authenticated())
                .addFilter(new JWTAuthenticationFilter(authenticationManager, sysUserService, jwtService, userCache()))
                .exceptionHandling().authenticationEntryPoint(new UserAuthenticationEntryPoint()).and()
                // 认证用户时用户信息加载配置,注入springAuthUserService
                .userDetailsService(sysUserService).build();
    }

    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     * @param authenticationConfiguration
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 密码明文加密方式配置(使用国密SM4)
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new SM4PasswordEncoder();
    }

    @Bean
    UserCache userCache(){
        Cache ca = cacheManager.getCache("userCache");
        return new SpringCacheBasedUserCache(ca);
    }

授权相关配置文件 AuthProperties

@Data
@ConfigurationProperties(prefix = "lc.security")
public class AuthProperties {

    private JWT jwt;

    private List<String> permitStatic;

    private List<String> permitMethod;

    @Data
    public static class JWT{

        private Claims claims = new Claims();
        private String authHeader;
        private String secret;
        private Type type = Type.RANDOM;

        public void setAuthHeader(String authHeader) {
            this.authHeader = authHeader;
        }

        public String getAuthHeader() {
            return authHeader;
        }

        public Claims getClaims() {
            return claims;
        }

        public void setSecret(String secret) {
            this.secret = secret;
        }

        public String getSecret() {
            return secret;
        }

        public void setType(Type type) {
            this.type = type;
        }

        public Type getType() {
            return type;
        }

        public enum Type {
            RANDOM, FOREVER
        }

        @Setter
        @Getter
        public static class Claims {
            private String issuer = "AppName";
            private String audience = "Web";
            private String subject = "Auth";
            private Long expirationTimeMinutes = 60L;
        }

    }

配置文件 application-security.yaml

lc:
  security:

    #静态资源放行
    permit-static:
      - /*.html
      - /*.html
      - /favicon.ico
      - /**/*.html
      - /**/*.css
      - /**/*.js
      - /**/*.png
      - /**/*.jpg
      - /**/*.ttf
      - /**/*.woff
      - /**/*.wav
      - /**/*.gif
      - /swagger-ui.html

    #方法放行
    permit-method:
      - /swagger-resources
      - /v2/api-docs
      - /v3/api-docs
      - /api/v1/sys/auth/login

    jwt:
      auth-header: Authorization
      secret: mySecret
      type: forever
      claims:
        issuer: LC
        audience: Web
        subject: Auth
        expiration-time-minutes: 3000

异常处理UserAuthenticationEntryPoint

public class UserAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.OK.value());
        response.getWriter().write(JSONObject.toJSONString(ReturnVO.failed("登录信息不正确!")));
        response.getWriter().flush();

    }
}

过滤器JWTAuthenticationFilter

@Slf4j
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

    private final SysUserService userService;
    private final JWTService jwtService;
    private final UserCache userCache;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, SysUserService userService, JWTService jwtService, UserCache userCache) {
        super(authenticationManager);
        this.userService = userService;
        this.jwtService = jwtService;
        this.userCache = userCache;
    }

    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String token = jwtService.getToken(request);
        String tokenHeader = request.getHeader("Authorization");
        // 如果请求头中没有Authorization信息则直接放行了
        if (!StringUtils.hasLength(tokenHeader)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果请求头中有token,则进行解析,并且设置认证信息
        String username = jwtService.validateToken(token);
        if (!StringUtils.hasLength(username)) {
            log.error("从token中未获取到用户名, token:{}, URI:{}", token, request.getServletPath());
            chain.doFilter(request, response);
            return;
        }

        //从缓存中验证token的存在性
        UserInfo user = (UserInfo) userCache.getUserFromCache(username);
        if (null == user) {
            try {
                user = (UserInfo) userService.loadUserByUsername(username);
                userCache.putUserInCache(user);
            } catch (UsernameNotFoundException e) {
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.setStatus(HttpStatus.OK.value());
                response.getWriter().write(JSONObject.toJSONString(ReturnVO.failed(e.getMessage())));
                response.getWriter().flush();
                return;
            }
        }
        // 如果从持久化存储中仍未查到,则执行后续操作,最后返回用户不存在信息到前端
        if (null != user) {
            // 清空“密码”属性
            // 创建验证通过的令牌对象
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
            // 设置令牌到安全上下文中
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}

JWTService 与 JWTServiceImpl

public interface JWTService {

    /**
     * 签名生成
     * @param username
     * @return
     */
    String generateToken(String username);
    /**
     * 签名检验
     * @param token
     * @return
     */
    String validateToken(String token);
    /**
     * 签名查询
     * @param request
     * @return
     */
    String getToken(HttpServletRequest request);
}

@Slf4j
@Service
public class JWTServiceImpl implements JWTService {

    private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;

    private AuthProperties properties;

    public JWTServiceImpl(AuthProperties properties) {
        this.properties = properties;
    }

    private Claims getAllClaims(String token) throws AuthTokenException {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(properties.getJwt().getSecret())
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            throw new AuthTokenException(e.getMessage());
        }
        return claims;
    }

    private Date generateExpirationDate() {
        return new Date(new Date().getTime() + properties.getJwt().getClaims().getExpirationTimeMinutes() * 60 * 1000);
    }

    private String getAuthHeader(HttpServletRequest request) {
        return request.getHeader(properties.getJwt().getAuthHeader());
    }

    @Override
    public String generateToken(String username) {
        return Jwts.builder()
                .setIssuer(properties.getJwt().getClaims().getIssuer())
                .setSubject(username)
                .setAudience(properties.getJwt().getClaims().getAudience())
                .setIssuedAt(new Date())
                .setExpiration(generateExpirationDate())
                .signWith(SIGNATURE_ALGORITHM, properties.getJwt().getSecret())
                .compact();
    }

    @Override
    public String validateToken(String token){
        Claims allClaims = null;
        try {
            return getAllClaims(token).getSubject();
        } catch (AuthTokenException e) {
            log.error(e.getMessage(), e);
        }
        return null;
    }

    @Override
    public String getToken(HttpServletRequest request) {
        return getAuthHeader(request);
    }

}

SysUserService

@Slf4j
@Service
public class SysUserService implements UserDetailsService {

    @Autowired
    private SysUserMapper userMapper;

   @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException    {

        UserInfo userInfo = userMapper.getUserInfoByUsername(username);
        ParamAssert.notNull(userInfo, "用户不存在!");
        return userInfo;
    }

}

用户信息

@Data
@ApiModel("用户")
@EqualsAndHashCode(callSuper = true)
public class UserInfo implements UserDetails {

    @ApiModelProperty(notes = "用户名")
    private String username;

    @ApiModelProperty(notes = "姓名")
    private String name;

    @ApiModelProperty(notes = "编码")
    private String code;

    @ApiModelProperty(notes = "密码")
    private String password;

    @ApiModelProperty(notes = "是否启用:true-启用,false-停用")
    private boolean enabled = true;
    
    private List<RoleAuthority> authorities;

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

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
}

测试

登录接口

启动项目,放开登录接口,登录

返回结果

{
  "code": 200,
  "message": "SUCCESS",
  "data": {
    "token": "eyJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJMQy1NSU5GQU5HIiwic3ViIjoiYWRtaW4iLCJhdWQiOiJXZWIiLCJpYXQiOjE2NjUyMjAzODMsImV4cCI6MTY2NTQwMDM4M30.MhqQl79CgevBw2zeDuL2tsxgZaUe43e16-kw0aWMfCD5Hs9NI_D0dlwwvvr0znlORf6y5eyzyao8EqVIv09URQ"
  }
}

登陆失败提示

标签: spring java 后端

本文转载自: https://blog.csdn.net/yu619251940/article/details/126872454
版权归原作者 ricardo.M.Yu 所有, 如有侵权,请联系我们删除。

“springboot 2.7整合spring security 5.7整合jwt实现用户登录注册与鉴权全记录”的评论:

还没有评论