SpringSecurity-14-SpringSecurity结合JWT实现前后端分离的后端授权

SpringSecurity-14-SpringSecurity结合JWT实现前后端分离的后端授权

什么是JWT

JWT是JSON WEB TOKEN的缩写,它是基于RFC 7519标准定义的一种可以安全传输的JSON对象,因为使用了数字签名,所以可以信任。

JWT的组成

  • JWT token的格式:header.payload.signature

  • header中用于存放签名的生成算法

    {"alg": "HS512"}
    
    
  • payload用于存放用户名、token的生成时间和过期时间

    {"sub":"admin","created":1489079981393,"exp":1489684781}
    
    
  • signature为以header和payload生成的签名,一旦header和payload被篡改,验证将失败

    //secret为加密算法的密钥
    String signature = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
    
    

JWT实例

这是一个JWT的字符串

eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2NDg5ODg1MjAsInN1YiI6ImFkbWluIiwiY3JlYXRlZCI6MTY0ODk4NDkyMDQyNX0.P8YJ5AhcKATEpUmdtSmzGXcdDacESZ2jqU20JpjCqZOqy5AEE2uelYtay--Kg2wRWFx3bBhf9A5Jbv2S8fbs_A

可以在该网站上获得解析结果:https://jwt.io/

SpringSecurity-14-SpringSecurity结合JWT实现前后端分离的后端授权_第1张图片

编码实现


环境准备工作

  • 建立Spring Boot项目并集成了Spring Security,项目可以正常启动

  • 通过controller写一个HTTP的GET方法服务接口,比如:“/student/selectall”

  • 实现最基本的动态数据验证及权限分配,即实现UserDetailsService接口。这两个接口都是向Spring Security提供用户、角色、权限等校验信息的接口

  • 如果你学习过Spring Security的formLogin登录模式,请将HttpSecurity配置中的formLogin()配置段全部去掉。因为JWT完全使用JSON接口,没有from表单提交。

  • HttpSecurity配置中一定要加上csrf().disable(),即暂时关掉跨站攻击CSRF的防御。这样是不安全的,我们后续章节再做处理。

以上实现可以去查看我的专题SpringBootSpringSecurity进行查看。

SpringSecurity-14-SpringSecurity结合JWT实现前后端分离的后端授权_第2张图片

在pom.xml中添加项目依赖

 
      
        
            org.springframework.boot
            spring-boot-starter-security
        
        
        
            io.jsonwebtoken
            jjwt
            0.9.0
        
        
            org.springframework.boot
            spring-boot-starter-web
        

        
        
            mysql
            mysql-connector-java
            runtime
        
        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.5.1
        
        
            cn.hutool
            hutool-all
            5.5.7
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.springframework.security
            spring-security-test
            test
        
    

在application.yml中加入如下自定义一些关于JWT的配置

jwt:
  header: JWTName 
  secret: springkhbd
  expiration: 360

  • jwt.header的value是Http的header中存储JWT的名称,名字可读性越差越安全

  • jwt.secret用来对JWT基础信息进行加密和解密的密匙。

  • jwt.expiration用来设置JWT令牌的有效时间

添加JWT token的工具类JwtTokenUtil

JwtTokenUtil用于生成和解析JWT token的工具类

主要方法:

  • generateToken(UserDetails userDetails):根据用户信息生成token令牌

  • getUserNameFromToken(String token):根据token令牌获取用户名

  • validateToken(String token, UserDetails userDetails):判断用户是否过期

  • refreshToken(String token):根据token属性token的过期时间

package com.security.learn.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Data
@Slf4j
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtTokenUtil {

    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    private String secret;
    private Long expiration;
    private String header;


    /**
     * 生成token令牌
     *
     * @param userDetails 用户
     * @return 令token牌
     */
    public String generateToken(UserDetails userDetails) {
        Map claims = new HashMap<>(2);
        claims.put(CLAIM_KEY_CREATED, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        //生成Token
        return generateToken(claims);
    }

    /**
     * 从claims生成令牌
     * @param claims
     * @return
     */
    private String generateToken(Map claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 从Token中获取用户名称
     * @param token
     * @return
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username =  claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 从令牌中获取数据声明,如果看不懂就看谁调用它
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            log.info("JWT格式验证失败:{}",token);
            claims = null;
        }
        return claims;
    }

    /**
     * 生成token的过期时间
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 根据token过去过期时间
     * @param token
     * @return
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     *
     * 验证Token是否过期
     * @param token
     * @param userDetails
     * @return true表示没有过期,false表示过期
     */
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 判断令牌是否过期
     * @param token
     * @return
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 判断token是否可以刷新
     * @param token
     * @return
     */
    public boolean canRefresh(String token) {
        return !isTokenExpired(token);
    }

    /**
     * 刷新token
     */
    public String refreshToken(String token) {
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }
}

UserDetailsService接口的实现

@Component("myUserDetailsService")
@Slf4j
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private AuthoritiesMapper authoritiesMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("认证请求: "+ username);
        QueryWrapper wrapper = new QueryWrapper<>();
        wrapper.eq("username",username);
        List userEntities = userMapper.selectList(wrapper);
        if (userEntities.size()>0){
            QueryWrapper wrapper1 = new QueryWrapper<>();
            wrapper.eq("userId", userEntities.get(0).getId());
            List authorities = authoritiesMapper.selectList(wrapper1);
            return new User(username, userEntities.get(0).getPassword(), AuthorityUtils.createAuthorityList(authorities.toString()));
        }
        return null;
    }
}

开发登录接口(获取Token的接口)

JwtAdminService接口

public interface JwtAdminService {

    /**
     * 登录功能
     * @param username 用户名
     * @param password 密码
     * @return 生成的JWT的token
     */
    String login(String username, String password);

    /**
     * 刷新Token
     * @param oldToken
     * @return
     */
    String refreshToken(String oldToken);
}


JwtAdminService接口实现

@AllArgsConstructor
@Slf4j
@Service
public class JwtAdminServiceImpl implements JwtAdminService {

    private final UserDetailsService customUserDetailsService;

    private final JwtTokenUtil jwtTokenUtill;

    private final PasswordEncoder passwordEncoder;

    /**
     * 根据用户名密码登录时生成Token
     * @param username 用户名
     * @param password 密码
     * @return
     */
    @Override
    public String login(String username, String password) {
        try{

            //根据用户名获取 用户信息
            UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);

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

            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);


            SecurityContextHolder.getContext().setAuthentication(token);

        }catch (AuthenticationException e){
            log.error("用户名或者密码不正确");
        }
        //生成JWT
        UserDetails userDetails = customUserDetailsService.loadUserByUsername( username );
        return jwtTokenUtill.generateToken(userDetails);
    }



    @Override
    public String refreshToken(String oldToken) {
        if (!jwtTokenUtill.isTokenExpired(oldToken)) {
            return jwtTokenUtill.refreshToken(oldToken);
        }
        return null;
    }
}

JwtAuthController的实现

  • "/login"接口用于登录验证,并且生成JWT返回给客户端

  • "/refreshtoken"接口用于刷新JWT,更新JWT令牌的有效期

@RestController
public class JwtAuthController {

    @Resource
    private JwtAdminService jwtAuthService;

    @PostMapping(value = "/login")
    public Result login(@RequestBody Map map) throws Exception {
        String username = map.get("username");
        String password = map.get("password");
        if (StrUtil.isEmpty(username) || (StrUtil.isEmpty(password))) {
            return Result.fail("用户名密码不能为空");
        }
        try{
            return Result.data( jwtAuthService.login(username, password));
        }catch(Exception e){
            return Result.fail(e.getMessage());
        }
    }

    @PostMapping(value = "/refreshtoken")
    public String refresh(@RequestHeader("${jwt.header}") String token) {
        return jwtAuthService.refreshToken(token);
    }
}


添加SpringSecurity的配置类LearnSrpingSecurity

import com.security.learn.filter.JwtAuthenticationTokenFilter;
import com.security.learn.handler.RestAuthenticationEntryPoint;
import com.security.learn.handler.RestfulAccessDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * 安全配置类
 */
@EnableWebSecurity
public class LearnSrpingSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService myUserDetailsService;

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    /**
     * 认证管理器
     * 1.认证信息提供方式(用户名、密码、当前用户的资源权限)
     * 2.可采用内存存储方式,也可能采用数据库方式等
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
    }

    /**
     * 资源权限配置(过滤器链):
     * 1、被拦截的资源
     * 2、资源所对应的角色权限
     * 3、定义认证方式:httpBasic 、httpForm
     * 4、定制登录页面、登录请求地址、错误处理方式
     * 5、自定义 spring security 过滤器
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable() //禁用跨站csrf攻击防御,后面的章节会专门讲解
                .sessionManagement()// 基于token,所以不需要session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/login").permitAll()//不需要通过登录验证就可以被访问的资源路径
                .anyRequest().authenticated();
        //添加自定义未授权和未登录结果返回
        http.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);
    }

}


相关依赖以及方法说明

  • configure(HttpSecurity http):资源权限配置(过滤器链)、jwt过滤器及出异常后的处理器;

  • configure(AuthenticationManagerBuilder auth):用于配置UserDetailsServicePasswordEncoder

  • RestfulAccessDeniedHandler:当用户没有访问权限时的处理器,用于返回JSON格式的处理结果;

  • RestAuthenticationEntryPoint:当未登录或token失效时,返回JSON格式的结果;

  • UserDetailsService:SpringSecurity定义的核心接口,用于根据用户名获取用户信息,需要自行实现;

  • JwtAuthenticationTokenFilter:在用户名和密码校验前添加的过滤器,如果有jwt的token,会自行根据token信息进行登录。

  • configure(HttpSecurity http),主要配置:

SpringSecurity-14-SpringSecurity结合JWT实现前后端分离的后端授权_第3张图片

  • 将我们的自定义jwtAuthenticationTokenFilter,加载到UsernamePasswordAuthenticationFilter的前面。

  • 因为我们使用了JWT,表明了我们的应用是一个前后端分离的应用,所以我们可以开启STATELESS禁止使用session

添加RestfulAccessDeniedHandler

当访问接口没有权限时,自定义的返回结果

/**
 * 当访问接口没有权限时,自定义的返回结果
 */
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(Result.fail(e.getMessage())));
        response.getWriter().flush();
    }
}

添加RestAuthenticationEntryPoint

当用户未登录或者token失效访问接口时,自定义的返回结果

/**
 * 当未登录或者token失效访问接口时,自定义的返回结果
 */
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(Result.fail(authException.getMessage())));
        response.getWriter().flush();
    }
}

添加JwtAuthenticationTokenFilter

在用户名和密码校验前添加的过滤器,如果请求中有jwt的token且有效,会取出token中的用户名,然后调用SpringSecurity的API进行登录操作。

@Slf4j
@Component
@AllArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private final UserDetailsService myUserDetailsService;

    private final JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String jwt = request.getHeader(jwtTokenUtil.getHeader());
        if(!StrUtil.isEmpty(jwt)){
            //根据jwt获取用户名
            String username = jwtTokenUtil.getUserNameFromToken(jwt);
            log.info("校验username:{}",username);
            //如果可以正确从JWT中提取用户信息,并且该用户未被授权
            if(!StrUtil.isEmpty(username) && SecurityContextHolder.getContext().getAuthentication()==null){
                UserDetails userDetails = this.myUserDetailsService.loadUserByUsername(username);
                if(jwtTokenUtil.validateToken(jwt,userDetails)){
                    //给使用该JWT令牌的用户进行授权
                    UsernamePasswordAuthenticationToken authenticationToken
                            = new UsernamePasswordAuthenticationToken(userDetails,null,
                            userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        filterChain.doFilter(request, response);
    }
}

测试

测试登录接口,即:获取token的接口。输入正确的用户名、密码即可获取token

SpringSecurity-14-SpringSecurity结合JWT实现前后端分离的后端授权_第4张图片

  • 使用不带token,但是不传递JWT令牌,结果是禁止访问

SpringSecurity-14-SpringSecurity结合JWT实现前后端分离的后端授权_第5张图片

  • 使用不带token,携带JWT令牌

SpringSecurity-14-SpringSecurity结合JWT实现前后端分离的后端授权_第6张图片

如果您觉得本文不错,欢迎关注,点赞,收藏支持,您的关注是我坚持的动力!
原创不易,转载请注明出处,感谢支持!如果本文对您有用,欢迎转发分享!

你可能感兴趣的:(SpringSecurity,springsecurity,前后端分离,restful,springboot)