第四章 SpringBoot快速开发框架 - Spring Security + JWT实现登录权限认证

作者简介:
🌹 作者:暗夜91
🤟 个人主页:暗夜91的主页
📝 如果感觉文章写的还有点帮助,请帮忙点个关注,我会持续输出高质量技术博文。


专栏文章:
1、集成Swagger,生成API文档
2、Mysql数据源配置
3、集成Redis
4、Spring Security + JWT实现登录权限认证
5、跨域配置

专栏源码:
针对该专栏功能,对源码进行整理,可以直接下载运行。
源码下载请移步:SpringBoot快速开发框架


四、Spring Security + JWT实现登录权限认证

1、Spring Security介绍

Spring Security是一个高度自定义的安全框架,利用Spring IOC和AOP的功能,为系统提供声明式的安全访问控制。

Spring Security最重要的核心功能就是**【认证】【授权】**,认证通俗的说就是判断用户是否成功登录。授权则是判断用户是否有权限去访问业务接口。

2、JWT

Json Web Token (JWT):为了在网络应用环境间传递声明而执行的一种基于JSON的开发标准(RFC97519),它定义 了一种紧凑的、自包含的方式,用于作为JSON对象安全的传输信息,是一种数字签名的方式。

JWT最常见的应用场景就是授权,一旦对用户进行授权,返回用户授权令牌(token),之后用户的所有请求必须携带token,用于对用户的权限进行验证。

JWT的结构

JWT有三部分组成,他们之间用圆点(.)连接,这三部分分别是:

  • header

header 由两部分组成,token的类型(JWT)和算法名称(比如SHA256或者RSA等),用BASE64对header进行编码就得到了JWT的第一部分

  • payload

它是JWT的第二部分,里面包含声明(要求),是关于用户或者其他数据的声明,有三种声明类型:registered, public 和 private。

    • Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
    • Public claims : 可以随意定义。
    • Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。
  • signature

JWT的签名部分,使用编码过的header和payload,然后通过密钥对信息进行加密签名得到。

3、集成方法

(1)引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

引入依赖后,启动服务可以看到日志中会出现security默认的密码,这里不做过多解释,因为之后会对登录进行自定义实现。

在这里插入图片描述

(2)创建用户实体类和对应的JPA接口

这里承接第二部中的数据源配置,使用JPA作为ORM框架

UserEntity

User的实体类,并且要实现UserDetails接口,之后会用到这个地方

package com.lll.framework.module.user.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Collection;

@Entity
@Table(name = "sys_user_info")
public class UserEntity implements UserDetails {

    @Id
    @GeneratedValue
    private Integer id;
    private String username;
    private String password;

    @Override
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

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

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

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

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

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

}

UserRepository

Jpa对于User类的查询接口,用于对用户的登录验证使用。

package com.lll.framework.module.user.repository;

import com.lll.framework.module.user.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<UserEntity,Integer> {
}

(3)自定义UserDetailsService

创建UserServiceImpl,并实现UserDetailsService,从数据库中获取用户信息,进行权限验证。

package com.lll.framework.module.user.service.impl;

import com.lll.framework.module.user.entity.UserEntity;
import com.lll.framework.module.user.repository.UserRepository;
import com.lll.framework.module.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.CollectionUtils;

import java.util.List;

public class UserServiceImpl implements UserService, UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    /**
     * 自定义用户验证信息
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<UserEntity> userList = userRepository.findByUsername(username);
        if (CollectionUtils.isEmpty(userList)){
            return new UserEntity();
        }
        UserEntity userEntity = userList.get(0);


        /** TODO
         *  这里是使用最小范围返回的用户信息
         *  可以加入实际的业务需求,比如用户的角色相关的信息等
         */

        return userEntity;
    }
}

(4)自定义退出类

package com.lll.framework.module.security.handle;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lll.framework.common.response.Response;
import com.lll.framework.common.response.ResponseUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        Response response = ResponseUtil.success();
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        PrintWriter out = httpServletResponse.getWriter();
        out.write(objectMapper.writeValueAsString(response));
        out.flush();
        out.close();
    }
}

(5)自定义权限验证失败类

package com.lll.framework.module.security.handle;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lll.framework.common.response.Response;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * 认证失败处理类 返回未授权
 **/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        System.out.println(e.getMessage());
        Response response = new Response();
        response.setCode(300);
        response.setMsg("未登陆");

        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.setStatus(403);
        httpServletResponse.getWriter().append(objectMapper.writeValueAsString(response));
    }
}

(6)新增Jwt工具类

Jwt的工具类,提供了生成token的公共方法。

package com.lll.framework.module.security.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;

public class JwtTokenUtil {

    private static Key key = new SecretKeySpec(new BCryptPasswordEncoder().encode("lll-framework").getBytes(), SignatureAlgorithm.HS512.getJcaName());

    @Value("${server.tokenDuration}")
    private int tokenDuration;

    public int getTokenDuration() {
        return tokenDuration;
    }

    /**
     * 生成token
     * @param username
     * @param tokenDuration 有效时长
     * @return
     */
    public String generateToken(String username,int tokenDuration ) {

        String token = Jwts.builder()
                .setClaims(null)
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + tokenDuration * 1000))
                .signWith(SignatureAlgorithm.HS512,key)
                .compact() ;
        return token;
    }

    /**
     * 解析token,如果已经过期,刷新token
     * @param token
     * @return
     */
    public String parseToken(String token) {
        String subject = null;
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token).getBody();
            subject = claims.getSubject();
        } catch (Exception e) {
            System.out.println(e.getLocalizedMessage());
            subject="taken无效";
        }
        return subject;
    }

}

(7)新增JwtAuthenticationTokenFilter

JWT全局拦截,对所有的API请求进行拦截,从请求header中获取token信息,并与redis中的缓存信息进行比对验证。

package com.lll.framework.module.security.filter;

import com.alibaba.fastjson.JSON;
import com.lll.framework.common.redis.RedisTemplateService;
import com.lll.framework.module.security.util.JwtTokenUtil;
import com.lll.framework.module.user.entity.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisTemplateService redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null) {
            String authToken;
            if (authHeader.contains("Bearer ")) {
                authToken = authHeader.substring(7);
            } else {
                authToken = authHeader;
            }
            boolean flag = redisTemplate.hasKey(authToken);
            if (flag) {
                String userInfo = redisTemplate.get(authToken, String.class);
                if (SecurityContextHolder.getContext().getAuthentication() == null) {
                    UserEntity userDetails = JSON.parseObject(userInfo,UserEntity.class);
                    if (userDetails != null) {
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }
        }
        chain.doFilter(request, response);
    }
}

(8)新增Spring Security配置文件

在配置文件中,对登录请求的验证进行配置,并指定请求的白名单,对白名单中的请求不进行拦截。

package com.lll.framework.config;

import com.lll.framework.module.security.filter.JwtAuthenticationTokenFilter;
import com.lll.framework.module.security.handle.AuthenticationEntryPointImpl;
import com.lll.framework.module.security.handle.LogoutSuccessHandlerImpl;
import com.lll.framework.module.security.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPoint;

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/sign/login", "/user/register").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/profile/**"
                ).permitAll()
                .antMatchers("/swagger-ui.html").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .antMatchers("/*/api-docs").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

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

(9)添加登录接口

Controller

package com.lll.framework.module.login.controller;

import com.lll.framework.common.response.Response;
import com.lll.framework.common.response.ResponseUtil;
import com.lll.framework.module.login.service.LoginService;
import com.lll.framework.module.login.vo.LoginVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: Liu Lili <br>
 */
@Api(tags = "登录")
@RestController
@RequestMapping("/sign")
public class LoginController {

    @Autowired
    private LoginService loginService;

    @ApiOperation("用户名密码登录")
    @PostMapping("/login")
    public Response loginByUsernameAndPwd(LoginVO loginVO) throws Exception {
        String token = loginService.login(loginVO.getUsername(), loginVO.getPassword());
        return ResponseUtil.success(token);
    }
}

ServiceImpl

package com.lll.framework.module.login.service.impl;


import com.lll.framework.common.redis.RedisTemplateService;
import com.lll.framework.module.login.service.LoginService;
import com.lll.framework.module.security.util.JwtTokenUtil;
import com.lll.framework.module.user.entity.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
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.stereotype.Service;

import javax.annotation.Resource;

@Service
public class LoginServiceImpl implements LoginService {

    @Resource
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private RedisTemplateService redisTemplate;

    @Override
    public String login(String username, String password) throws Exception {
        Authentication authentication = null;
        try {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        } catch (Exception e) {
            throw new Exception();
        }
        UserEntity loginUser = (UserEntity) authentication.getPrincipal();
        String token = jwtTokenUtil.generateToken(loginUser.getUsername());
        redisTemplate.set(token,loginUser);
        return token;
    }
}

到这里Spring Security的集成就完成了,小伙伴们可以愉快的进行用户的登录和权限验证了,下面j写一个注册接口和一个查询全部用户接口做一下测试。

(10)测试

  • 对注册接口需要加入白名单,无需token便可直接访问
  • 对查询全部用户接口,则需验证通过才能进行访问

Controller

package com.lll.framework.module.user.controller;

import com.lll.framework.common.response.Response;
import com.lll.framework.common.response.ResponseUtil;
import com.lll.framework.module.user.entity.UserEntity;
import com.lll.framework.module.user.service.UserService;
import com.lll.framework.module.user.vo.UserVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/user")
@Api(tags = "用户管理")
public class UserController {

    @Autowired
    private UserService userService;

    @ApiOperation(value = "用户注册")
    @PostMapping("register")
    public Response saveUserInfo(@RequestBody UserVO user){
        Boolean flag = userService.saveUserInfo(user);
        return ResponseUtil.success(flag);
    }

    @ApiOperation(value = "获取全部用户")
    @GetMapping("/list")
    public Response getUserList(){
        List<UserEntity> list = userService.getUserList();
        return ResponseUtil.success(list);
    }

}

ServiceImpl

package com.lll.framework.module.user.service.impl;

import com.lll.framework.module.user.entity.UserEntity;
import com.lll.framework.module.user.repository.UserRepository;
import com.lll.framework.module.user.service.UserService;
import com.lll.framework.module.user.vo.UserVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Boolean saveUserInfo(UserVO user) {
        String pwd = user.getPassword();
        user.setPassword(passwordEncoder.encode(pwd));
        UserEntity entity = new UserEntity();
        entity.setUsername(user.getUsername());
        entity.setPassword(user.getPassword());
        entity.setEnabled(true);
        userRepository.save(entity);
        return true;
    }

    @Override
    public List<UserEntity> getUserList() {
        return userRepository.findAll();
    }
}

测试截图

1、注册用户,因为加入白名单中,所以无需Authorization

在这里插入图片描述

2、获取全部用户,请求时需在header中携带Authorization,否则会返回提示用户登录

在这里插入图片描述
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暗夜91

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值