SpringBoot+Security+Jwt登录认证与权限控制(一)

一、相关技术

1. Maven 项目管理工具

2. MybatisPlus

3. SpringBoot 2.7.0

4. Security 安全框架

5. Jwt

6. easy-captcha 验证码

7. swagger2 3.3.0

        swagger2的3.3.0版本相关配置可以看我的相关博客(SpringBoot整合Swagger2)

二、技术简介

1. Security

1.1 简介

        Spring Security是Spring家族中的一个重量级安全管理框架,实际上,在Spring Boot出现之前,Spring Security就已经发展了很多年了。Spring Boot为Spring Security提供了自动化配置方案。可以零配置使用Spring Security。

扫描二维码关注公众号,回复: 14931858 查看本文章

1.2 认证方式

  • form表单认证【推荐】
  • httpBasic认证

2. Jwt

2.1 简介

        Json Web Token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准,特别适用于分布式站点的单点登录(SSO)场景。

JWT广义:JWT就是签发token和校验token的一种机制。

JWT狭义:JWT就是token

        基于token的鉴权机制类似于http协议也是无状态的,他不需要在服务端去保留用户的认证信息或者会话信息,这就意味着基于token认证机制的应用不需要考虑用户在哪一台服务器登陆了 这就为应用的扩展提供了便利。

2.2 官网图片描述

        官网地址:JSON Web Token Introduction - jwt.io

JSON Web Token Introduction - jwt.io

2.3 Jwt组成

        Jwt由三部分组成,头部、有效载荷、签名。

2.3.1 头部

        头部用于描述该Jwt的最基本的信息。可以被表示成一个JSON对象。

 2.3.2 载荷(Playload)

        载荷就是存放有效信息的地方。有效信息包含以下部分:

(1)七个默认字段供选择(供选用)

  • iss (issuer):签发人

  • exp (expiration time):过期时间

  • sub (subject):主题

  • aud (audience):受众

  • nbf (Not Before):生效时间

  • iat (Issued At):签发时间

  • jti (JWT ID):编号

(2)私有的声明

        私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64

是对称解密的,意味着该部分信息可以归类为明文信息。

2.3.3 签名

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

  • 需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。

三、代码展示

1. pom.xml

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

        <!--easy-captcha 验证码-->
        <dependency>
            <groupId>com.github.whvcse</groupId>
            <artifactId>easy-captcha</artifactId>
            <version>1.6.2</version>
        </dependency>

        <!-- java-jwt 可以反编码过期token-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.18.3</version>
        </dependency>

2. SecurityConfig.java(Security安全配置类)

/**
 * @author w
 * @createDate 2022/6/13
 * @description: 安全配置类
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 自定义用户认证逻辑
     */
    @Resource
    private MyUserDetailService userDetailService;
    @Resource
    private MyAuthenticationSuccessHandler successHandler;
    @Resource
    private MyAuthenticationFailureHandler failureHandler;
    /**
     * 认证不通过的处理类
     */
    @Resource
    private MyAuthenticationEntryPointHandler entryPointHandler;
    /**
     * 令牌认证过滤
     */
    @Resource
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    /**
     * 匹配器
     */
    @Resource
    private JwtAuthenticationProvider provider;

    @Resource
    private MyLogoutSuccessHandler logoutSuccessHandler;
    @Resource
    private CorsFilter corsFilter;

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

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // jdbc
        auth.userDetailsService(userDetailService);
        // 自定义匹配器
        auth.authenticationProvider(provider);
    }

    /**
     * 自定义登录页面的页面放行
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 没有token认证不通过
                .exceptionHandling().authenticationEntryPoint(entryPointHandler)
                .and()
                // 基于token不使用session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 过滤请求
                .authorizeRequests()
                // 允许匿名访问,防止重定向锁死
                .antMatchers("/captchaImage","/login").anonymous()
                .antMatchers("/swagger-ui/**").anonymous()
                .antMatchers("/swagger/**").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/v2/**").anonymous()
                // 除了以上,均要鉴权
                .anyRequest()
                .authenticated()
                .and() // 登出成功处理
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(logoutSuccessHandler);

        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(corsFilter,JwtAuthenticationFilter.class);

    }
}

 3. JwtUtil.java(令牌工具类)

/**
 * 令牌工具类
 */
public class JwtUtil {

    // 服务器的密钥
    private static String secret = "123456";

    /**
     * 生成令牌
     *
     * @param claims
     * @return
     */
    public static String genToken(Map<String, String> claims , int expire) {
        // 有效时间
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.MINUTE, expire);

        // 载荷
        JWTCreator.Builder builder = JWT.create();
        claims.forEach((k,v)->builder.withClaim(k,v));

        // 设置有效期和签名=>生成令牌
        String token = builder.withExpiresAt(instance.getTime())
                .sign(Algorithm.HMAC256(secret));
        return token;
    }

    /**
     * 校验令牌
     *
     * @param token
     */
    public static void verifyToken(String token) {
        JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
    }

    /**
     * 解析令牌
     *
     * @param token
     * @return
     */
    public static String parseToken(String token,String key) {
        DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
        return decodedJWT.getClaim(key).asString();
    }

    /**
     * 反编码过期令牌
     * @param token
     * @param key
     * @return
     */
    public static String decodeExpireToken(String token, String key) {
        return JWT.decode(token).getClaim(key).asString();
    }
}

3. JwtAuthenticationFilter.java(令牌过滤器)

/**
 * @author w
 * @createDate 2022/6/13
 * @description: 令牌过滤器
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        if(requestURI.equals("/captchaImage") || requestURI.equals("/login")){
            filterChain.doFilter(request,response);
            return;
        }
        String accessToken = getTokenByRequest(request);
        if(StrUtil.isEmpty(accessToken)){
            response.setContentType("text/html;charset=utf-8");
            PrintWriter out = response.getWriter();
            out.write("没有令牌,请先登录");
            out.close();
            return;
        }
        // 把令牌放置到Principal
        JwtToken jwtToken = new JwtToken(accessToken);
        // 存储上下文认证信息
        try {
            SecurityContextHolder.getContext().setAuthentication(jwtToken);
        } catch (Exception e) {
            if(e.getCause()!=null && e.getCause() instanceof TokenExpiredException){
                responseFailResult(response,AjaxResult.fail(1001,"访问令牌过期"));
                return;
            }
            // 用户已经退出/用户在其他地方登录
            responseFailResult(response,AjaxResult.fail(1002,e.getMessage()));
            return;
        }
        filterChain.doFilter(request,response);
    }

    @SneakyThrows
    private void responseFailResult(HttpServletResponse response, AjaxResult result) {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.println(JSONUtil.toJsonStr(result));
    }
    /**
     * 从请求头里获取访问令牌
     * @param request
     * @return
     */
    private String getTokenByRequest(HttpServletRequest request) {
        String tokenStr = request.getHeader("Authorization");
        if(StrUtil.isNotEmpty(tokenStr) && tokenStr.startsWith("Bearer ")){
            // 返回令牌
            return tokenStr.replace("Bearer ","");
        }
        return "";
    }
}

 4. MyAuthenticationEntryPointHandler.java(认证失败处理)

/**
 * @author w
 * @createDate 2022/6/13
 * @description: 认证失败的处理类,返回未授权
 */
@Component
public class MyAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        Map<String,Object> map = new HashMap<>();
        map.put("status",402);
        map.put("msg",e.getMessage());
        PrintWriter out = response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }
}

 5. MyLogoutSuccessHandler.java(登出处理)

/**
 * 登出处理
 */
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        Map<String,Object> map = new HashMap<>();
        map.put("status",200);
        map.put("msg","登出成功");
        PrintWriter out = response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }
}

6. JwtAuthenticationProvider.java(自定义匹配器)

/**
 * @author w
 * @createDate 2022/6/14
 * @description: 自定义匹配器
 */
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(JwtToken.class);
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String accessToken = (String) authentication.getPrincipal();

        //try {
            JwtUtil.verifyToken(accessToken);
        //} catch (Exception e) {
        //    throw new AuthenticationCredentialsNotFoundException("令牌校验失败");
        //}

        String userNo = JwtUtil.parseToken(accessToken, Constants.USERID);
        // 获取redis的访问令牌
        RedisTemplate redisTemplate = RedisBean.redis;
        String refreshToken = (String) redisTemplate.opsForValue().get(Constants.REFRESH_TOKEN_PREFIX + userNo);
        if(ObjectUtil.isEmpty(refreshToken)){
            throw new AuthenticationCredentialsNotFoundException("用户已退出");
        }
        // 判断时间戳
        String accessTokenCurrentTime = JwtUtil.parseToken(accessToken, Constants.CURRENTTIMEMILLIS);
        String refreshTokenCurrentTime = JwtUtil.parseToken(refreshToken, Constants.CURRENTTIMEMILLIS);
        if(!accessTokenCurrentTime.equalsIgnoreCase(refreshTokenCurrentTime)){
            throw new AuthenticationCredentialsNotFoundException("用户已在别处登录");
        }
        return new JwtToken(accessToken);
    }


}

7. JwtToken.java(认证令牌)

/**
 * @author w
 * @createDate 2022/6/14
 * @description: 认证令牌
 */
public class JwtToken extends AbstractAuthenticationToken {
    private String token;

    public JwtToken(String token) {
        super((Collection)null);
        this.setAuthenticated(false);
        this.token = token;
    }

    public JwtToken(Collection<? extends GrantedAuthority> authorities, String token) {
        super(authorities);
        this.setAuthenticated(true);
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return null;
    }
}

8. 登录的controller service serviceImpl

8.1 LoginController.java(登录控制层)

@RestController
public class LoginController {
    @Resource
    private LoginServiceI loginService;

    @PostMapping("/login")
    public AjaxResult login(LoginVo loginVo){
        AjaxResult ajaxResult = AjaxResult.success();
        ajaxResult.put("token",loginService.login(loginVo));
        return ajaxResult;
    }
}

8.2 LoginServiceI.java(登录业务逻辑接口)

public interface LoginServiceI {
    String login(LoginVo loginVo);
}

8.3 LoginServiceImpl.java(登录业务逻辑)

@Service
public class LoginServiceImpl implements LoginServiceI {
    @Resource
    private SysUserMapper sysUserMapper;

    @Resource
    private RedisTemplate<String,String> redisTemplate;

    /**
     * 登录成功返回访问令牌给前端
     * @param loginVo
     * @return
     */
    @Override
    public String login(LoginVo loginVo) {

        validateCaptcha(loginVo.getCode(), loginVo.getUuid());

        // 验证用户名和密码
        SysUser sysUserDB = sysUserMapper.selectByUserName(loginVo.getUsername());
        if(ObjectUtil.isEmpty(sysUserDB)){
            // 用户名不存在
            throw new CustomException(CommonCode.USERNAME_ISNOT_EXIST);
        }
        if("1".equals(sysUserDB.getStatus())){
            throw new CustomException(CommonCode.USER_OUTAGE);
        }
        if("2".equals(sysUserDB.getDelFlag())){
            throw new CustomException(CommonCode.USER_IS_DELETE);
        }
        if(!PwdUtil.encode(loginVo.getPassword(),loginVo.getUsername()).equalsIgnoreCase(sysUserDB.getPassword())){
            throw new CustomException(CommonCode.USERNAME_OR_PASSWORD_ERROR);
        }

        // 生成令牌
        // 获得系统毫秒数
        long currentTimeMillis = System.currentTimeMillis();
        // 载荷信息,JWT令牌中不存放敏感信息
        Map<String, String> claims = new HashMap<>();
        claims.put(Constants.USERID,String.valueOf(sysUserDB.getUserId()));
        claims.put(Constants.USERNAME,sysUserDB.getUserName());
        claims.put(Constants.CURRENTTIMEMILLIS,String.valueOf(currentTimeMillis));
        // 访问令牌返回给前端
        String accessToken = JwtUtil.genToken(claims,Constants.EXPIRE_ACCESS_TOKEN_TIME);
        // 刷新令牌写入redis
        String refreshToken = JwtUtil.genToken(claims, Constants.EXPIRE_REFRESH_TOKEN_TIME);
        redisTemplate.opsForValue().set(Constants.REFRESH_TOKEN_PREFIX+sysUserDB.getUserId(),refreshToken,Constants.EXPIRE_REFRESH_TOKEN_TIME, TimeUnit.MINUTES);
        return accessToken;
    }

    /**
     * 验证码认证
     * @param code
     * @param uuid
     */
    private void validateCaptcha(String code, String uuid) {
        String key = Constants.CAPTCHA_CODE_PREFIX + uuid;
        String realCode = redisTemplate.opsForValue().get(key);
        if(StrUtil.isNotEmpty(realCode)){
            if(code.equalsIgnoreCase(realCode)){
                redisTemplate.delete(key);
                return;
            }else {
                throw new CustomException(CommonCode.CAPTCHA_ERROR);
            }
        }else {
            throw new CustomException(CommonCode.CAPTCHA_EXPIRE);
        }

    }

9. 授权

        在controller层使用注解@PreAuthorize("hasAuthority('xxx:xxx:xxx')")进行授权。

例如:

UserController.java

        使用一个方法举例。

@RestController
@RequestMapping("/system/user")
@Api(tags = "用户信息表接口")
public class SysUserController {
     /**
     * 用户信息表业务层
     */
    @Resource
    private SysUserServiceI sysUserService;
    
     /**
     * 增加用户信息表
     * @param sysUser
     * @return
     */
    @PostMapping
    @ApiOperation("增加用户信息表")
    @PreAuthorize("hasAuthority('system:user:add')")
    public AjaxResult add(@RequestBody SysUser sysUser){
        sysUserService.add(sysUser);
        return AjaxResult.success();
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_48568302/article/details/125385404