SpringSecurity + jwt 实现登录认证

SpringSecurity

SpringSecurity是一个强大的可高度定制的认证和授权框架,对于Spring应用来说它是一套Web安全标准。SpringSecurity注重于为Java应用提供认证和授权功能,像所有的Spring项目一样,它对自定义需求具有强大的扩展性。

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.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE1NTY3NzkxMjUzMDksImV4cCI6MTU1NzM4MzkyNX0.d-iki0193X0bBOETf2UN3r3PotNIEAV7mzIxxeI5IxFyzzkOZxS0PGfF_SK6wxCv2K8S0cZjMkv6b5bCqc0VBw

可以在该网站上获得解析结果:https://jwt.io/
1.Web安全配置:

package com.auth.authserver.config;

import com.auth.authserver.filter.JwtLoginFilter;
import com.auth.authserver.filter.JwtVerifyFilter;
import com.auth.authserver.handle.MyAuthenticationEntryPoint;
import com.auth.authserver.handle.MyAccessDeniedHandle;
import com.auth.authserver.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author Json
 * @date 2021/10/29 15:08
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private PasswordEncoder passwordEncoder;
    private UserService userService;
    private MyAccessDeniedHandle myAccessDeniedHandle;
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    @Autowired
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @Autowired
    public void setRestfulAccessDeniedHandle(MyAccessDeniedHandle myAccessDeniedHandle) {
        this.myAccessDeniedHandle = myAccessDeniedHandle;
    }

    @Autowired
    public void setRestAuthenticationEntryPoint(MyAuthenticationEntryPoint myAuthenticationEntryPoint) {
        this.myAuthenticationEntryPoint = myAuthenticationEntryPoint;
    }

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

    /**
     * Remove the ROLE_ prefix
     */
    @Bean
    public GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults("");
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                // 允许访问
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated() // 其他请求拦截
                .and()
                .csrf().disable() //关闭csrf
                .addFilter(new JwtLoginFilter(super.authenticationManager()))
                .addFilter(new JwtVerifyFilter(super.authenticationManager()))
                .exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandle) // 自定义无权限访问
                .authenticationEntryPoint(myAuthenticationEntryPoint) // 自定义未登录返回
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //禁用session
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // UserDetailsService类
        auth.userDetailsService(userService)
                // 加密策略
                .passwordEncoder(passwordEncoder);

    }

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

2.定义接口UserService去继承UserDetailsService

/**
 * @author Json
 * @date 2021/10/29 15:11
 */
public interface UserService extends UserDetailsService {
    
}

3.定义实现类UserServiceImpl 实现UserService

package com.auth.authserver.service.impl;
import com.auth.authserver.service.UserService;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Json
 * @date 2021/10/29 15:11
 */
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List authorities = new ArrayList<>();
        // 用户可以访问的资源名称(或者说用户所拥有的权限) 注意:必须"ROLE_"开头
        authorities.add(new SimpleGrantedAuthority("ADMIN"));
        // 临时写死 这里是数据库查询出来的
        return User.builder().username("admin")
                .password(passwordEncoder.encode("123456"))
                .authorities(authorities).build();

    }
}

4.自定义用户名密码登录,也就是UsernamePasswordAuthenticationFilter,重写认证逻辑,其实也就是登录接口,默认地址为"/login" 请求为POST,这里面包含了认证,登录成功该怎么办,登录失败该怎么办,当然也可以自己去定义登录接口,其实到道理是一样的

package com.auth.authserver.filter;
import cn.hutool.json.JSONUtil;
import com.auth.authserver.utils.JwtUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * 登录校验
 *
 * 第一种方式 我们这里用框架自带的 过滤器实现
 * 第二种方式 可以自己实现登录接口  去认证 其实也是 AuthenticationManager。authenticate 去认证
 *
 * @author Json
 * @date 2021/11/1 13:47
 */
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
    private final AuthenticationManager authenticationManager;

    public JwtLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 相当于登录 认证
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        setDetails(request, authenticationToken);
        return authenticationManager.authenticate(authenticationToken);
    }

    /**
     * 一旦调用 springSecurity认证登录成功,立即执行该方法
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        //登录成功时,返回json格式进行提示
        String username = obtainUsername(request);
        String jwt = JwtUtils.createJwt(username);
        response.setContentType("application/json;charset=utf-8");
        Map map = new HashMap<>(4);
        // 这里写死只做测试  请以实际为主
        map.put("code", "200");
        map.put("message", "登陆成功!");
        map.put("token",jwt);
        response.addHeader("Authorization",  jwt);
        response.getWriter().println(JSONUtil.parse(map));
        response.getWriter().flush();
        response.getWriter().close();
    }

    /**
     * 一旦调用 springSecurity认证失败 ,立即执行该方法
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) throws IOException {
        //登录失败时,返回json格式进行提示
        Map map = new HashMap(4);
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
        PrintWriter out = response.getWriter();
        if (ex instanceof BadCredentialsException) {
            map.put("code", HttpServletResponse.SC_BAD_GATEWAY);
            map.put("message", "账号或密码错误!");
        }else {
            // 这里还有其他的 异常 。。 比如账号锁定  过期 等等。。。
            map.put("code", HttpServletResponse.SC_BAD_GATEWAY);
            map.put("message", "登陆失败!");
        }
        out.write(new ObjectMapper().writeValueAsString(map));
        response.getWriter().println(JSONUtil.parse(map));
        response.getWriter().flush();
        response.getWriter().close();
    }
}

5.自定义请求拦截器,为什么呢,难道你可以随意请求吗?配置文件中写好了除了登录接口可以通过,其他请求全部拦截下来。这里是用jwt做的。

package com.auth.authserver.filter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * 请求校验
 *
 * @author Json
 * @date 2021/11/6 14:34
 */
public class JwtVerifyFilter extends BasicAuthenticationFilter {

    public JwtVerifyFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader("Authorization");
        if (header != null) {
            List authorities = new ArrayList<>();
            // 用户可以访问的资源名称(或者说用户所拥有的权限) 注意:必须"ROLE_"开头
            authorities.add(new SimpleGrantedAuthority("user:resource"));
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken
                    ("admin",null, authorities);
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
     
        }
        chain.doFilter(request, response);
    }
}

6.增加当访问接口没有权限时的处理

package com.auth.authserver.handle;
import cn.hutool.json.JSONUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 当访问接口没有权限时主要实现springsecurity给我们提供的 AccessDeniedHandler接口,自定义的返回结果
 *
 * @author Json
 * @date 2021/11/10 11:04
 */
@Component
public class MyAccessDeniedHandle implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        // 这里写死只做测试  请以实际为主
        Map map = new HashMap<>();
        map.put("code", 501);
        map.put("msg", "您没有权限");
        response.getWriter().println(JSONUtil.parse(map));
        response.getWriter().flush();
    }
}

7.当未登录或者token失效访问接口时自定义处理

package com.auth.authserver.handle;
import cn.hutool.json.JSONUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 当未登录或者token失效访问接口时,自定义的返回结果
 *
 *
 * @author Json
 * @date 2021/11/10 11:20
 */
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        Map map = new HashMap<>(4);
        // 这里写死只做测试  请以实际为主
        map.put("code", HttpServletResponse.SC_BAD_GATEWAY);
        map.put("message", "请登录!");
        response.getWriter().println(JSONUtil.parse(map));
        response.getWriter().flush();
    }
}

附上jwt工具类,当然以实际为主,这里主要做测试,封装的很简单

package com.crm.common.utils;

import com.crm.common.config.JwtConfig;
import com.crm.common.enums.ResultCodeEnum;
import com.crm.common.exception.JwtApiException;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;

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

/**
 * jwt 工具类
 *
 * @author Json
 * @date 2021/11/15 16:38
 */
@Slf4j
public class JwtUtils {
    /**
     * jwt 载荷信息 key
     */
    public static final String JWT_PAYLOAD_USER_KEY = "user";

    /**
     * 刷新token次数 默认为0起始
     */
    public static final String REFRESH_TOKEN_NUMBER = "refreshTokenNumber";

    /**
     * access-token
     */
    public static final String ACCESS_TOKEN = "access-token";

    /**
     * 加密token
     *
     * @param userInfo  载荷中的数据
     * @param jwtConfig jwt 配置
     * @return JWT
     */
    public static String createAccessToken(Object userInfo, JwtConfig jwtConfig) {
        Map map = new HashMap<>();
        map.put(JWT_PAYLOAD_USER_KEY, userInfo);
        map.put(REFRESH_TOKEN_NUMBER, 0);
        return Jwts.builder()
                .setClaims(map)
                .setId(createJTI())
                .setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getAccessTokenExpire() * 1000))
                .signWith(SignatureAlgorithm.HS256, jwtConfig.getAccessTokenSecret())
                .compact();
    }


    /**
     * 生成 RefreshToken
     *
     * @return refreshToken
     */
    public static String createRefreshToken(Object userInfo, JwtConfig jwtConfig) {
        return createRefreshToken(userInfo, jwtConfig, 0);
    }


    /**
     * 生成 RefreshToken
     *
     * @return refreshToken
     */
    public static String createRefreshToken(Object userInfo, JwtConfig jwtConfig, int refreshTokenNumber) {
        Map map = new HashMap<>();
        map.put(JWT_PAYLOAD_USER_KEY, userInfo);
        map.put(REFRESH_TOKEN_NUMBER, refreshTokenNumber);
        return Jwts.builder()
                .setClaims(map)
                .setId(createJTI())
                .setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getRefreshTokenExpire() * 1000))
                .signWith(SignatureAlgorithm.HS256, jwtConfig.getRefreshTokenSecret())
                .compact();
    }


    /**
     * 解析 refreshToken
     *
     * @param token     token
     * @param jwtConfig 配置项
     * @return 载荷信息
     */
    public static Claims parserAccessToken(String token, JwtConfig jwtConfig) {
        return parserToken(token, jwtConfig.getAccessTokenSecret());
    }

    /**
     * 解析  token
     *
     * @param token     token
     * @param jwtConfig 配置项
     * @return 载荷信息
     */
    public static Claims parserRefreshToken(String token, JwtConfig jwtConfig) {
        return parserToken(token, jwtConfig.getRefreshTokenSecret());
    }

    /**
     * 获取token中的载荷信息
     *
     * @param token 用户请求中的令牌
     * @return 用户信息
     */
    public static Claims parserToken(String token, String secretKey) {
        try {
            return Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            log.error("token{}过期", token, e);
            throw new JwtApiException(ResultCodeEnum.JWT_EXPIRED.code(), ResultCodeEnum.JWT_EXPIRED.message());
        } catch (SignatureException e) {
            log.error("token=[{}], 签名", token, e);
            throw new JwtApiException(ResultCodeEnum.JWT_SIGNATURE.code(), ResultCodeEnum.JWT_SIGNATURE.message());
        } catch (Exception e) {
            log.error("token=[{}]解析错误 message:{}", token, e.getMessage(), e);
            throw new JwtApiException(ResultCodeEnum.JWT_ERROR.code(), ResultCodeEnum.JWT_ERROR.message());
        }
    }


    private static String createJTI() {
        return new String(java.util.Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
    }
}

你可能感兴趣的:(SpringSecurity + jwt 实现登录认证)