Spring Security整合JWT实现前后端分离认证和权限管理(超详细)

核心步骤

  • 创建项目
  • 配置pom.xml
  • 在配置文件中写入jwt相关配置,并创建JWT的配置类,使用@ConfigurationProperties(prefix = “jwt”)与配置文件关联起来
  • 创建自己的用户类
  • 创建自己的无凭证处理类
  • 创建自己的认证失败类
  • 创建自己的权限不足类
  • 创建自己的认证成功处理类
  • 创建自己的UserDetailsService
  • 创建JWT工具类
  • 创建自定义的Token过滤器
  • 创建自己的Spring Secrity配置类(将之前的自定义的配置全部设置进去)

一、创建项目

默认创建Spring Boot项目即可

项目目录:
Spring Security整合JWT实现前后端分离认证和权限管理(超详细)_第1张图片

二、配置pom.xml

		 <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-configuration-processorartifactId>
            <optional>trueoptional>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.73version>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-tomcatartifactId>
            <scope>providedscope>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        
        
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwtartifactId>
            <version>0.9.0version>
        dependency>

三、配置JWT相关设置

配置这个主要是为了方便更改jwt相关的一些配置属性,比如加密的时候使用的盐值,token的过期时间等等,我们可以使用@ConfigurationProperties(prefix = "jwt")将配置文件与类联系起来,方便在开发过程中使用

  • 配置文件
#请求头
jwt.header=Authorization
#盐值
jwt.base64-secret=meng
#过期时间
jwt.token-validity-in-seconds=14400000
  • 对应实体类
@Data
@ToString
@Configuration
@ConfigurationProperties(prefix = "jwt") //与配置文件中的数据关联起来(这个注解会自动匹配jwt开头的配置)
public class JwtProperties {
    /** Request Headers : Authorization */
    private String header;

    /** Base64对该令牌进行编码 */
    private String base64Secret;

    /** 令牌过期时间 此处单位/毫秒 */
    private Long tokenValidityInSeconds;
}

四、创建自己的用户类

最好实现UserDetails接口,可以方面我们后面的使用,当然也可以不实现,但是在一些地方需要返回UserDeatils类型的数据,你得再自己做一次转换,很麻烦
注意: 在实现UserDetails接口后,会让你实现下面的一堆方法,你要看清每一个方法都是返回什么信息的,然后对它进行更改,因为你刚刚实现这些方法时,它返回的要么是null,要么是false,下面这个是我改过的。

@Data
public class JwtUser implements UserDetails {   //实现UserDeails接口
    //用户名
    private String username;
    //密码
    private String password;
    // 权限(角色)列表
    Collection<? extends GrantedAuthority> authorities;

    public JwtUser(String stuId, String password, List<GrantedAuthority> grantedAuthorities) {
        this.username = stuId;
        this.password = password;
        this.authorities = grantedAuthorities;
    }


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

    @Override
    public String getUsername() {
        return this.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;
    }
}

五、自定义无凭证处理类及认证失败处理类

  • 无凭证处理类

当用户没有携带有效凭证时,就会转到这里来,当然,我们还需要在Spring Security的配置类中指定我们自定义的处理类才可以

/**
 * 认证失败处理类
 */
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        System.out.println("无凭证");
        Result r = new Result();
        r.code(ResultCode.UNAUTHORIZED).message("无凭证");
        // 使用fastjson
        String json =  JSON.toJSONString(r);
        // 指定响应格式是json
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(json);
    }
}
  • 自定义认证失败类

当用户输入错误的账号或者密码时,就会进入这个处理类,同样要在配置类中指明(这个类上面的图片中没有,因为我第一个版本没写,这个类应当放到security包下)

/**
 * 自定义认证失败处理类
 */
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        returnFailure(httpServletResponse);
    }
    public void returnFailure(HttpServletResponse response) throws IOException{
        Result r = new Result();
        r.code(ResultCode.UNAUTHORIZED).message("认证失败");
        // 使用fastjson
        String json =  JSON.toJSONString(r);
        // 指定响应格式是json
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(json);
    }
}

六、创建自定义权限不足处理类

同样需要在配置类中添加

/**
 * 自定义无权访问处理类
 */
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        Result r = new Result();
        r.code(ResultCode.FORBIDDEN).message("权限不足");
        String json =  JSON.toJSONString(r);
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(json);
    }
}

七、JWT工具类

这个我是直接复制的别人的,经过自己稍微的修改用起来的,核心的功能就那么几个,只要能与自己的功能对应上就可以,比如我这个刚拿过来的时候它的好多配置都是写死的,而我的一些配置都在配置文件中,那就只需要找到相应位置,改成自己的就行

@Component
public class JwtTokenUtil {
	// 注入自己的jwt配置
    @Resource
    private JwtProperties jwtProperties;

    static final String CLAIM_KEY_USERNAME = "sub";
    static final String CLAIM_KEY_AUDIENCE = "audience";
    static final String CLAIM_KEY_CREATED = "created";

    private static final String AUDIENCE_UNKNOWN = "unknown";
    private static final String AUDIENCE_WEB = "web";
    private static final String AUDIENCE_MOBILE = "mobile";
    private static final String AUDIENCE_TABLET = "tablet";

    public String getUsernameFromToken(String token) {
        String username;
        try {
            final Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    public Date getCreatedDateFromToken(String token) {
        Date created;
        try {
            final Claims claims = getClaimsFromToken(token);
            created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
        } catch (Exception e) {
            created = null;
        }
        return created;
    }

    public Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            final Claims claims = getClaimsFromToken(token);
            //得到token的有效期
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        return expiration;
    }

    public String getAudienceFromToken(String token) {
        String audience;
        try {
            final Claims claims = getClaimsFromToken(token);
            audience = (String) claims.get(CLAIM_KEY_AUDIENCE);
        } catch (Exception e) {
            audience = null;
        }
        return audience;
    }

    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(jwtProperties.getBase64Secret())
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    //设置过期时间
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + jwtProperties.getTokenValidityInSeconds());
//        return new Date(30 * 24 * 60);
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        return (lastPasswordReset != null && created.before(lastPasswordReset));
    }

    // Device用户检测当前用户的设备,用不到的话可以删掉(使用这个需要添加相应的依赖)
//    private String generateAudience(Device device) {
//        String audience = AUDIENCE_UNKNOWN;
//        if (device.isNormal()) {
//            audience = AUDIENCE_WEB;
//        } else if (device.isTablet()) {
//            audience = AUDIENCE_TABLET;
//        } else if (device.isMobile()) {
//            audience = AUDIENCE_MOBILE;
//        }
//        return audience;
//    }

    private Boolean ignoreTokenExpiration(String token) {
        String audience = getAudienceFromToken(token);
        return (AUDIENCE_TABLET.equals(audience) || AUDIENCE_MOBILE.equals(audience));
    }

    public String generateToken(String username) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, username);
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /**
     * 生成token(最关键)
     * @param claims
     * @return
     */
    String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)  //设置声明信息(用户名等)
                .setExpiration(generateExpirationDate()) //设置过期时间
                .signWith(SignatureAlgorithm.HS512, jwtProperties.getBase64Secret()) //设置签名
                .compact();
    }

    public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
        final Date created = getCreatedDateFromToken(token);
        return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
                && (!isTokenExpired(token) || ignoreTokenExpiration(token));
    }

    public String refreshToken(String token) {
        String refreshedToken;
        try {
            final Claims claims = getClaimsFromToken(token);
            claims.put(CLAIM_KEY_CREATED, new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    //TODO,验证当前的token是否有效
    public Boolean validateToken(String token, UserDetails userDetails) {
        JwtUser user = (JwtUser) userDetails;
        final String username = getUsernameFromToken(token);
        final Date created = getCreatedDateFromToken(token);
        return (username.equals(user.getUsername())&& !isTokenExpired(token));
    }
}

八、自定义认证成功处理类(关键)

当用户认证成功之后,我们要在这里为用户生成token,并返回给用户,需要用到我们自定义的jwt工具类,也需要在配置类中配置

/**
 * 自定义认证成功处理器
 */

@Component
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //生成token
        final String realToken = jwtTokenUtil.generateToken(authentication.getName());
        HashMap<String,Object> map = new HashMap<>();
        map.put("token", realToken);
        Result r = new Result();
        r.code(ResultCode.SUCCESS).message("登录成功").data(map);

        //将生成的authentication放入容器中,生成安全的上下文
        SecurityContextHolder.getContext().setAuthentication(authentication);

        String json =  JSON.toJSONString(r);
        httpServletResponse.setContentType("text/json;charset=utf-8");
        httpServletResponse.getWriter().write(json);
    }

九、自定义UserDeailsService

在这里我们要实现用户信息的查询,将查询到的信息返回给Spring Security,让它进行信息的对比,在比对过后会跳转到相应的处理类

这里应该是要到数据库中去查询,我这里暂时写成固定的了

@Service
public class JwtUserDetailServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //暂时写成固定的
        if(!s.equals("admin")) return null;//用户不是admin,报错
        System.out.println("查询"+s);
        return new JwtUser("admin","$2a$10$WtN/BQbwY8dI0me.JsLxP.yyGePyTMg3bi3GZeRogowB4ZuoL1zrK", AuthorityUtils.commaSeparatedStringToAuthorityList("user"));
    }
}

通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”)来创建authorities集合对象的。参数是一个字符串,多个权限使用逗号分隔。

  • 角色授权:授权代码需要加ROLE_前缀,controller上使用时不要加前缀
  • 权限授权:设置和使用时,名称保持一致即可
  • 数据库查询版:
@Service
@Transactional
public class JwtUserDetailServiceImpl implements UserDetailsService {
    @Resource
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        QueryWrapper<Admin> wrapper = new QueryWrapper<>();
        wrapper.eq("username",userName);
        Admin admin = this.userMapper.selectOne(wrapper);
        if (admin == null){
            throw new UsernameNotFoundException("用户名不存在");
        }
        return admin;
    }
}

十、创建自定义的Token过滤器

这个过滤器的主要作用是为了在用户登录并获取到我们发配的token之后,在带着token发送请求时,我们要检验token,判断它是否携带着token,token是否过期,token中的用户是否包含在我们的数据库中等等,如果token有效,则直接让Spring Security形成安全上下文,不再进行验证


@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Resource
    private UserDetailsService userDetailsService;

    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @Resource
    private JwtProperties jwtProperties;



    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        如果在前端测试时出现跨域问题,到收藏的博客里面看一看

        String requestUrl = httpServletRequest.getRequestURI();

        String authToken = httpServletRequest.getHeader(jwtProperties.getHeader());

        String stuId = jwtTokenUtil.getUsernameFromToken(authToken);


        System.out.println("进入自定义过滤器");

        System.out.println("自定义过滤器获得用户名为   "+stuId);

        //当token中的username不为空时进行验证token是否是有效的token
        if (stuId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            //token中username不为空,并且Context中的认证为空,进行token验证
            //TODO,从数据库得到带有密码的完整user信息
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(stuId);


            if (jwtTokenUtil.validateToken(authToken, userDetails)) { //如username不为空,并且能够在数据库中查到
                /**
                 * UsernamePasswordAuthenticationToken继承AbstractAuthenticationToken实现Authentication
                 * 所以当在页面中输入用户名和密码之后首先会进入到UsernamePasswordAuthenticationToken验证(Authentication),
                 * 然后生成的Authentication会被交由AuthenticationManager来进行管理
                 * 而AuthenticationManager管理一系列的AuthenticationProvider,
                 * 而每一个Provider都会通UserDetailsService和UserDetail来返回一个
                 * 以UsernamePasswordAuthenticationToken实现的带用户名和密码以及权限的Authentication
                 */
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));


                //将authentication放入SecurityContextHolder中
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

十一、Spring Security配置类

这个配置类里面我们要之前的自定义配置全部加进去,并且对路由什么的进行配置

/**
 * @ClassName: WebSecurityConfig
 * @Description: TODO Spring Security 配置类
 * @Author 孟祥龙
 * @Date: 2021/4/13 8:52
 * @Version 1.0
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Resource
    private JwtAccessDeniedHandler jwtAccessDeniedHandler;

    @Resource
    private JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler;

    @Resource
    private LoginFailureHandler loginFailureHandler;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 自定义的Jwt Token过滤器
    @Bean
    public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
        return new JwtAuthenticationTokenFilter();
    }




    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .formLogin()
                //自定义认证成功处理器
                .successHandler(jwtAuthenticationSuccessHandler)
                // 自定义失败拦截器
                .failureHandler(loginFailureHandler)
                // 自定义登录拦截URI
                .loginProcessingUrl("/login")
                .and()
                //token的验证方式不需要开启csrf的防护
                .csrf().disable()
                // 自定义认证失败类
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
                // 自定义权限不足处理类
                .accessDeniedHandler(jwtAccessDeniedHandler)
                .and()
                //设置无状态的连接,即不创建session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers("/login").permitAll()
                //配置允许匿名访问的路径
                .anyRequest().authenticated();
        // 解决跨域问题(重要)  只有在前端请求接口时才发现需要这个
        httpSecurity.cors().and().csrf().disable();


        //配置自己的jwt验证过滤器
        httpSecurity
                .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);


        // disable page caching
        httpSecurity.headers().cacheControl();
    }
}

关于@EnableGlobalMethodSecurity:https://blog.csdn.net/chihaihai/article/details/104678864

注意:Spring Security 默认的加密方式就是BCrypt,如果想要详细了解请自行百度

十二、控制器

@RestController
public class AuthController {

    @RequestMapping("/get")
    public Result get(){
        HashMap map = new HashMap();
        map.put("username","admin");
        map.put("password","123456");
        Result r = new Result();
        r.code(ResultCode.SUCCESS).message("成功访问").data(map);
        return r;
    }

    @PreAuthorize("hasAuthority('admin')")
    @RequestMapping("/del")
    public String del(){
        return "删除成功";
    }

}

十三、测试

  • 登录测试

Spring Security整合JWT实现前后端分离认证和权限管理(超详细)_第2张图片

  • 权限测试

Spring Security整合JWT实现前后端分离认证和权限管理(超详细)_第3张图片
Spring Security整合JWT实现前后端分离认证和权限管理(超详细)_第4张图片

源码地址:https://github.com/mengxianglong123/SpringSecurity-Demo/tree/master
注意: 此代码并不完全正确,应该是缺少了一部分代码的,请对比观看。

你可能感兴趣的:(Java高级框架,spring,spring,boot,jwt,java,Spring,Security)