当前位置: 首页 > news >正文

SpringSecurity和JWT实现认证和授权

SpringSecurity和JWT实现认证和授权

  • 框架介绍
    • SpringSecurity
    • JWT
      • 组成
      • 实例
      • JWT实现认证和授权的原理
    • Hutool
  • 使用表
  • 整合SpringSecurity及JWT
    • 在pom.xml中添加依赖
    • 添加JWT token的工具类
    • 添加RbacAdminService:
    • 添加自定义mapper
    • 创建SpringSecurity配置类
    • 添加ProjectSecurityConfig
    • 添加JwtAuthenticationTokenFilter
    • 添加congtroller
    • 修改Swagger的配置
  • 认证与授权流程演示
    • 运行项目,访问API
    • 未登录时访问接口
    • 登录后访问接口
    • 改用其他有权限的帐号登录
  • 总结
  • 源码

本文主要讲解SpringBoot项目通过整合SpringSecurity和JWT实现后台用户的登录和授权功能,同时使用Swagger-UI使其可以自动记住登录令牌进行发送。

框架介绍

SpringSecurity

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

JWT

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

组成

  • 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的字符串

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

使用线上解析工具:https://jwt.io/
在这里插入图片描述

JWT实现认证和授权的原理

  • 用户调用登录接口,登录成功后获取到JWT的token;
  • 之后用户每次调用接口都在http的header中添加一个叫Authorization的头,值为JWT的token;
  • 后台程序通过对Authorization头中信息的解码及数字签名校验来获取其中的用户信息,从而实现认证和授权。

Hutool

Hutool是一个丰富的Java开源工具包,它帮助我们简化每一行代码,减少每一个方法,mall项目采用了此工具包。

使用表

pms_brand:品牌表
rbac_admin:后台用户表
rbac_role:后台用户角色表
rbac_permission:后台用户权限表
rbac_admin_role_relation:后台用户和角色关系表,用户与角色是多对多关系
rbac_role_permission_relation:后台用户角色和权限关系表,角色与权限是多对多关系
rbac_admin_permission_relation:后台用户和权限关系表(除角色中定义的权限以外的加减权限),加权限是指用户比角色多出的权限,减权限是指用户比角色少的权限

CREATE TABLE `pms_brand` (`id` bigint NOT NULL AUTO_INCREMENT,`name` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,`first_letter` varchar(8) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '首字母',`sort` int DEFAULT NULL,`factory_status` int DEFAULT NULL COMMENT '是否为品牌制造商:0->不是;1->是',`show_status` int DEFAULT NULL,`product_count` int DEFAULT NULL COMMENT '产品数量',`product_comment_count` int DEFAULT NULL COMMENT '产品评论数量',`logo` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '品牌logo',`big_pic` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '专区大图',`brand_story` text CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci COMMENT '品牌故事',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=71 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='品牌表';CREATE TABLE `rbac_admin` (`id` bigint NOT NULL AUTO_INCREMENT,`username` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,`password` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,`icon` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '头像',`email` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '邮箱',`nick_name` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '昵称',`note` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '备注信息',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`login_time` datetime DEFAULT NULL COMMENT '最后登录时间',`status` int DEFAULT '1' COMMENT '帐号启用状态:0->禁用;1->启用',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='后台用户表';CREATE TABLE `rbac_role` (`id` bigint NOT NULL AUTO_INCREMENT,`name` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '名称',`description` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '描述',`admin_count` int DEFAULT NULL COMMENT '后台用户数量',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`status` int DEFAULT '1' COMMENT '启用状态:0->禁用;1->启用',`sort` int DEFAULT '0',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='后台用户角色表';CREATE TABLE `rbac_permission` (`id` bigint NOT NULL AUTO_INCREMENT,`pid` bigint DEFAULT NULL COMMENT '父级权限id',`name` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '名称',`value` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '权限值',`icon` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '图标',`type` int DEFAULT NULL COMMENT '权限类型:0->目录;1->菜单;2->按钮(接口绑定权限)',`uri` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '前端资源路径',`status` int DEFAULT NULL COMMENT '启用状态;0->禁用;1->启用',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`sort` int DEFAULT NULL COMMENT '排序',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='后台用户权限表';CREATE TABLE `rbac_admin_permission_relation` (`id` bigint NOT NULL AUTO_INCREMENT,`admin_id` bigint DEFAULT NULL,`permission_id` bigint DEFAULT NULL,`type` int DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='后台用户和权限关系表(除角色中定义的权限以外的加减权限)';CREATE TABLE `rbac_admin_role_relation` (`id` bigint NOT NULL AUTO_INCREMENT,`admin_id` bigint DEFAULT NULL,`role_id` bigint DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='后台用户和角色关系表';CREATE TABLE `rbac_role_permission_relation` (`id` bigint NOT NULL AUTO_INCREMENT,`role_id` bigint DEFAULT NULL,`permission_id` bigint DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='后台用户角色和权限关系表';

自行插入几条数据,做好用户、角色和权限关系对应,用户可以使用功能的注册接口,因为密码需要加密

整合SpringSecurity及JWT

在pom.xml中添加依赖

<!--SpringSecurity依赖配置-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--Hutool Java工具包-->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>4.5.7</version>
</dependency>
<!--JWT(Json Web Token)登录支持-->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version>
</dependency>

SpringBoot 2.6.0以上版本需要加个这个依赖

<dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version>
</dependency>

SpringBoot 2.6.0以上版本使用的swagger是3.0.0版本

		<!-- Swagger3 --><dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version></dependency><!--Swagger-UI API文档生产工具--><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>3.0.0</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>3.0.0</version></dependency>

添加JWT token的工具类

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

相关方法说明:

  • generateToken(UserDetails userDetails) :用于根据登录用户信息生成token
  • getUserNameFromToken(String token):从token中获取登录用户的信息
  • validateToken(String token, UserDetails userDetails):判断token是否还有效

先添加yml配置:

server:port: 8080spring:datasource:url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=falseusername: rootpassword: rootmvc:pathmatch:matching-strategy: ant_path_matchermybatis:mapper-locations:- classpath:dao/*.xml- classpath*:com/**/mapper/*.xml
logging:level:root: infocom.sheep: debug
# 自定义jwt key
jwt:tokenHeader: Authorization #JWT存储的请求头secret: mySecret #JWT加解密使用的密钥expiration: 604800 #JWT的超期限时间(60*60*24)tokenHead: Bearer  #JWT负载中拿到开头secure:ignored:urls: #安全路径白名单- /swagger-ui/- /swagger-resources/**- /**/v2/api-docs- /**/*.html- /**/*.js- /**/*.css- /**/*.png- /favicon.ico- /actuator/**- /druid/**- /admin/**
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.util.Date;
import java.util.HashMap;
import java.util.Map;/*** JwtToken生成的工具类*/
@Component
public class JwtTokenUtil {private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);private static final String CLAIM_KEY_USERNAME = "sub";private static final String CLAIM_KEY_CREATED = "created";@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private Long expiration;/*** 根据负责生成JWT的token*/private String generateToken(Map<String, Object> claims) {return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate()).signWith(SignatureAlgorithm.HS512, secret).compact();}/*** 从token中获取JWT中的负载*/private Claims getClaimsFromToken(String token) {Claims claims = null;try {claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();} catch (Exception e) {LOGGER.info("JWT格式验证失败:{}", token);}return claims;}/*** 生成token的过期时间*/private Date generateExpirationDate() {return new Date(System.currentTimeMillis() + expiration * 1000);}/*** 从token中获取登录用户名*/public String getUserNameFromToken(String token) {String username;try {Claims claims = getClaimsFromToken(token);username = claims.getSubject();} catch (Exception e) {username = null;}return username;}/*** 验证token是否还有效** @param token       客户端传入的token* @param userDetails 从数据库中查询出来的用户信息*/public boolean validateToken(String token, UserDetails userDetails) {String username = getUserNameFromToken(token);return username.equals(userDetails.getUsername()) && !isTokenExpired(token);}/*** 判断token是否已经失效*/private boolean isTokenExpired(String token) {Date expiredDate = getExpiredDateFromToken(token);return expiredDate.before(new Date());}/*** 从token中获取过期时间*/private Date getExpiredDateFromToken(String token) {Claims claims = getClaimsFromToken(token);return claims.getExpiration();}/*** 根据用户信息生成token*/public String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());claims.put(CLAIM_KEY_CREATED, new Date());return generateToken(claims);}/*** 判断token是否可以被刷新*/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);}
}

使用Mybatis Generator生成相关表的model mapper代码,使用方式参考链接: MyBatis Generator使用总结

生成如图代码:
在这里插入图片描述

添加RbacAdminService:

public interface RbacAdminService {/*** 根据用户名获取后台管理员*/RbacAdmin getAdminByUsername(String username);UserDetails loadUserByUsername(String username);/*** 注册功能*/RbacAdmin register(RbacAdmin umsAdminParam);/*** 登录功能** @param username 用户名* @param password 密码* @return 生成的JWT的token*/String login(String username, String password);/*** 获取用户所有权限(包括角色权限和+-权限)*/List<RbacPermission> getPermissionList(Long adminId);
}
@Service
public class RbacAdminServiceImpl implements RbacAdminService {private static final Logger LOGGER = LoggerFactory.getLogger(RbacAdminServiceImpl.class);@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Autowiredprivate PasswordEncoder passwordEncoder;@Value("${jwt.tokenHead}")private String tokenHead;@Autowiredprivate RbacAdminMapper adminMapper;@Autowiredprivate RbacAdminRoleRelationDao adminRoleRelationDao;@Overridepublic RbacAdmin getAdminByUsername(String username) {RbacAdminExample example = new RbacAdminExample();example.createCriteria().andUsernameEqualTo(username);List<RbacAdmin> adminList = adminMapper.selectByExample(example);if (adminList != null && adminList.size() > 0) {return adminList.get(0);}return null;}@Overridepublic UserDetails loadUserByUsername(String username) {//获取用户信息RbacAdmin admin = getAdminByUsername(username);if (admin != null) {List<RbacPermission> permissionList = getPermissionList(admin.getId());return new AdminUserDetails(admin, permissionList);}throw new UsernameNotFoundException("用户名或密码错误");}@Overridepublic RbacAdmin register(RbacAdmin umsAdminParam) {RbacAdmin rbacAdmin = new RbacAdmin();BeanUtils.copyProperties(umsAdminParam, rbacAdmin);rbacAdmin.setCreateTime(new Date());rbacAdmin.setStatus(1);//查询是否有相同用户名的用户RbacAdminExample example = new RbacAdminExample();example.createCriteria().andUsernameEqualTo(rbacAdmin.getUsername());List<RbacAdmin> umsAdminList = adminMapper.selectByExample(example);if (umsAdminList.size() > 0) {return null;}//将密码进行加密操作String encodePassword = passwordEncoder.encode(rbacAdmin.getPassword());rbacAdmin.setPassword(encodePassword);adminMapper.insert(rbacAdmin);return rbacAdmin;}@Overridepublic String login(String username, String password) {String token = null;//密码需要客户端加密后传递try {UserDetails userDetails = loadUserByUsername(username);if (!passwordEncoder.matches(password, userDetails.getPassword())) {throw new RuntimeException("密码不正确");}if (!userDetails.isEnabled()) {throw new RuntimeException("帐号已被禁用");}UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authentication);token = jwtTokenUtil.generateToken(userDetails);updateLoginTimeByUsername(username);} catch (AuthenticationException e) {LOGGER.warn("登录异常:{}", e.getMessage());}return token;}/*** 根据用户名修改登录时间*/private void updateLoginTimeByUsername(String username) {RbacAdmin record = new RbacAdmin();record.setLoginTime(new Date());RbacAdminExample example = new RbacAdminExample();example.createCriteria().andUsernameEqualTo(username);adminMapper.updateByExampleSelective(record, example);}@Overridepublic List<RbacPermission> getPermissionList(Long adminId) {return adminRoleRelationDao.getPermissionList(adminId);}
}

添加自定义mapper

RbacAdminRoleRelationDao 获取用户所有权限

public interface RbacAdminRoleRelationDao {/*** 获取用户所有权限(包括+-权限)*/List<RbacPermission> getPermissionList(@Param("adminId") Long adminId);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sheep.dao.RbacAdminRoleRelationDao"><select id="getPermissionList" resultMap="com.sheep.mbg.mapper.RbacPermissionMapper.BaseResultMap">SELECTp.*FROMrbac_admin_role_relation arLEFT JOIN rbac_role r ON ar.role_id = r.idLEFT JOIN rbac_role_permission_relation rp ON r.id = rp.role_idLEFT JOIN rbac_permission p ON rp.permission_id = p.idWHEREar.admin_id = #{adminId}AND p.id IS NOT NULLAND p.id NOT IN (SELECTp.idFROMrbac_admin_permission_relation prLEFT JOIN rbac_permission p ON pr.permission_id = p.idWHEREpr.type = - 1AND pr.admin_id = #{adminId})UNIONSELECTp.*FROMrbac_admin_permission_relation prLEFT JOIN rbac_permission p ON pr.permission_id = p.idWHEREpr.type = 1AND pr.admin_id = #{adminId}</select>
</mapper>

创建SpringSecurity配置类

dto包下创建一个AdminUserDetails, SpringSecurity需要的用户详情:

public class AdminUserDetails implements UserDetails {private RbacAdmin umsAdmin;private List<RbacPermission> permissionList;public AdminUserDetails(RbacAdmin umsAdmin, List<RbacPermission> permissionList) {this.umsAdmin = umsAdmin;this.permissionList = permissionList;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {//返回当前用户的权限return permissionList.stream().filter(permission -> permission.getValue()!=null).map(permission ->new SimpleGrantedAuthority(permission.getValue())).collect(Collectors.toList());}@Overridepublic String getPassword() {return umsAdmin.getPassword();}@Overridepublic String getUsername() {return umsAdmin.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return umsAdmin.getStatus().equals(1);}
}

添加 SecurityConfig:


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {@Autowiredprivate RestfulAccessDeniedHandler restfulAccessDeniedHandler;@Autowiredprivate RestAuthenticationEntryPoint restAuthenticationEntryPoint;@Autowiredprivate IgnoreUrlsConfig ignoreUrlsConfig;@BeanSecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();//不需要保护的资源路径允许访问for (String url : ignoreUrlsConfig.getUrls()) {registry.antMatchers(url).permitAll();}//允许跨域请求的OPTIONS请求registry.antMatchers(HttpMethod.OPTIONS).permitAll();httpSecurity.csrf()// 由于使用的是JWT,我们这里不需要csrf.disable().sessionManagement()// 基于token,所以不需要session.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().anyRequest()// 除上面外的所有请求全部需要鉴权认证.authenticated();// 禁用缓存httpSecurity.headers().cacheControl();// 添加JWT filterhttpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);//添加自定义未授权和未登录结果返回httpSecurity.exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler).authenticationEntryPoint(restAuthenticationEntryPoint);return httpSecurity.build();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {return new JwtAuthenticationTokenFilter();}}

IgnoreUrlsConfig 配置访问权限白名单

@Configuration
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {private List<String> urls = new ArrayList<>();public List<String> getUrls() {return urls;}public void setUrls(List<String> urls) {this.urls = urls;}
}

其中 RestAuthenticationEntryPoint 是 当未登录或者token失效访问接口时,自定义的返回结果:

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

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

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

相关依赖及方法说明

  • configure(HttpSecurity httpSecurity):用于配置需要拦截的url路径、jwt过滤器及出异常后的处理器;
  • configure(AuthenticationManagerBuilder auth):用于配置UserDetailsService及PasswordEncoder;
  • RestfulAccessDeniedHandler:当用户没有访问权限时的处理器,用于返回JSON格式的处理结果;
  • RestAuthenticationEntryPoint:当未登录或token失效时,返回JSON格式的结果;
  • UserDetailsService:SpringSecurity定义的核心接口,用于根据用户名获取用户信息,需要自行实现;
  • UserDetails:SpringSecurity定义用于封装用户信息的类(主要是用户信息和权限),需要自行实现;
  • PasswordEncoder:SpringSecurity定义的用于对密码进行编码及比对的接口,目前使用的是BCryptPasswordEncoder;
  • JwtAuthenticationTokenFilter:在用户名和密码校验前添加的过滤器,如果有jwt的token,会自行根据token信息进行登录

添加ProjectSecurityConfig

@Configuration
public class ProjectSecurityConfig {@Autowiredprivate RbacAdminService adminService;@Beanpublic UserDetailsService userDetailsService() {//获取登录用户信息return username -> {UserDetails admin = adminService.loadUserByUsername(username);if (admin != null) {return admin;}throw new UsernameNotFoundException("用户名或密码错误");};}
}

添加JwtAuthenticationTokenFilter

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

/*** JWT登录授权过滤器*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Value("${jwt.tokenHeader}")private String tokenHeader;@Value("${jwt.tokenHead}")private String tokenHead;@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws ServletException, IOException {String authHeader = request.getHeader(this.tokenHeader);if (authHeader != null && authHeader.startsWith(this.tokenHead)) {String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "String username = jwtTokenUtil.getUserNameFromToken(authToken);LOGGER.info("checking username:{}", username);if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);if (jwtTokenUtil.validateToken(authToken, userDetails)) {UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));LOGGER.info("authenticated user:{}", username);SecurityContextHolder.getContext().setAuthentication(authentication);}}}chain.doFilter(request, response);}
}

添加congtroller

/*** 后台用户管理*/
@Controller
@Api(tags = "RbacAdminController", description = "后台用户管理")
@RequestMapping("/admin")
public class RbacAdminController {@Autowiredprivate RbacAdminService adminService;@Value("${jwt.tokenHeader}")private String tokenHeader;@Value("${jwt.tokenHead}")private String tokenHead;@ApiOperation(value = "用户注册")@RequestMapping(value = "/register", method = RequestMethod.POST)@ResponseBodypublic CommonResult<RbacAdmin> register(@RequestBody RbacAdmin umsAdminParam, BindingResult result) {RbacAdmin umsAdmin = adminService.register(umsAdminParam);if (umsAdmin == null) {CommonResult.failed();}return CommonResult.success(umsAdmin);}@ApiOperation(value = "登录以后返回token")@RequestMapping(value = "/login", method = RequestMethod.POST)@ResponseBodypublic CommonResult login(@RequestBody RbacAdminLoginParam rbacAdminLoginParam, BindingResult result) {String token = adminService.login(rbacAdminLoginParam.getUsername(), rbacAdminLoginParam.getPassword());if (token == null) {return CommonResult.validateFailed("用户名或密码错误");}Map<String, String> tokenMap = new HashMap<>();tokenMap.put("token", token);tokenMap.put("tokenHead", tokenHead);return CommonResult.success(tokenMap);}@ApiOperation("获取用户所有权限(包括+-权限)")@RequestMapping(value = "/permission/{adminId}", method = RequestMethod.GET)@ResponseBodypublic CommonResult<List<RbacPermission>> getPermissionList(@PathVariable Long adminId) {List<RbacPermission> permissionList = adminService.getPermissionList(adminId);return CommonResult.success(permissionList);}
}

品牌表的service接口

public interface PmsBrandService {List<PmsBrand> listAllBrand();int createBrand(PmsBrand brand);int updateBrand(Long id, PmsBrand brand);int deleteBrand(Long id);List<PmsBrand> listBrand(int pageNum, int pageSize);PmsBrand getBrand(Long id);
}
/*** PmsBrandService实现类*/
@Service
public class PmsBrandServiceImpl implements PmsBrandService {@Autowiredprivate PmsBrandMapper brandMapper;@Overridepublic List<PmsBrand> listAllBrand() {return brandMapper.selectByExample(new PmsBrandExample());}@Overridepublic int createBrand(PmsBrand brand) {return brandMapper.insertSelective(brand);}@Overridepublic int updateBrand(Long id, PmsBrand brand) {brand.setId(id);return brandMapper.updateByPrimaryKeySelective(brand);}@Overridepublic int deleteBrand(Long id) {return brandMapper.deleteByPrimaryKey(id);}@Overridepublic List<PmsBrand> listBrand(int pageNum, int pageSize) {PageHelper.startPage(pageNum, pageSize);return brandMapper.selectByExample(new PmsBrandExample());}@Overridepublic PmsBrand getBrand(Long id) {return brandMapper.selectByPrimaryKey(id);}
}

品牌管理Controller,用于测试授权和权限认证


/*** 品牌管理Controller*/
@Api(tags = "PmsBrandController", description = "商品品牌管理")
@Controller
@RequestMapping("/brand")
public class PmsBrandController {@Autowiredprivate PmsBrandService brandService;private static final Logger LOGGER = LoggerFactory.getLogger(PmsBrandController.class);@ApiOperation("获取所有品牌列表")@RequestMapping(value = "listAll", method = RequestMethod.GET)@ResponseBody@PreAuthorize("hasAuthority('pms:brand:read')")public CommonResult<List<PmsBrand>> getBrandList() {return CommonResult.success(brandService.listAllBrand());}@ApiOperation("添加品牌")@RequestMapping(value = "/create", method = RequestMethod.POST)@ResponseBodypublic CommonResult createBrand(@RequestBody PmsBrand pmsBrand) {CommonResult commonResult;int count = brandService.createBrand(pmsBrand);if (count == 1) {commonResult = CommonResult.success(pmsBrand);LOGGER.debug("createBrand success:{}", pmsBrand);} else {commonResult = CommonResult.failed("操作失败");LOGGER.debug("createBrand failed:{}", pmsBrand);}return commonResult;}@ApiOperation("更新指定id品牌信息")@RequestMapping(value = "/update/{id}", method = RequestMethod.POST)@ResponseBodypublic CommonResult updateBrand(@PathVariable("id") Long id, @RequestBody PmsBrand pmsBrandDto, BindingResult result) {CommonResult commonResult;int count = brandService.updateBrand(id, pmsBrandDto);if (count == 1) {commonResult = CommonResult.success(pmsBrandDto);LOGGER.debug("updateBrand success:{}", pmsBrandDto);} else {commonResult = CommonResult.failed("操作失败");LOGGER.debug("updateBrand failed:{}", pmsBrandDto);}return commonResult;}@ApiOperation("删除指定id的品牌")@RequestMapping(value = "/delete/{id}", method = RequestMethod.GET)@ResponseBodypublic CommonResult deleteBrand(@PathVariable("id") Long id) {int count = brandService.deleteBrand(id);if (count == 1) {LOGGER.debug("deleteBrand success :id={}", id);return CommonResult.success(null);} else {LOGGER.debug("deleteBrand failed :id={}", id);return CommonResult.failed("操作失败");}}@ApiOperation("分页查询品牌列表")@RequestMapping(value = "/list", method = RequestMethod.GET)@ResponseBodypublic CommonResult<CommonPage<PmsBrand>> listBrand(@RequestParam(value = "pageNum", defaultValue = "1")@ApiParam("页码") Integer pageNum,@RequestParam(value = "pageSize", defaultValue = "3")@ApiParam("每页数量") Integer pageSize) {List<PmsBrand> brandList = brandService.listBrand(pageNum, pageSize);return CommonResult.success(CommonPage.restPage(brandList));}@ApiOperation("获取指定id的品牌详情")@RequestMapping(value = "/{id}", method = RequestMethod.GET)@ResponseBodypublic CommonResult<PmsBrand> brand(@PathVariable("id") Long id) {return CommonResult.success(brandService.getBrand(id));}
}

修改Swagger的配置

通过修改配置实现调用接口自带Authorization头,这样就可以访问需要登录的接口了。


/*** Swagger2API文档的配置*/
@Configuration
public class Swagger2Config {@Beanpublic Docket createRestApi() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()//为当前包下controller生成API文档.apis(RequestHandlerSelectors.basePackage("com.sheep.controller")).paths(PathSelectors.any()).build()//添加登录认证.securitySchemes(securitySchemes()).securityContexts(securityContexts());}private ApiInfo apiInfo() {return new ApiInfoBuilder().title("SwaggerUI演示").description("learn-SecurityAndJWT").contact(new Contact("learn-SecurityAndJWT",null,null)).version("1.0").build();}private List<SecurityScheme> securitySchemes() {//设置请求头信息List<SecurityScheme> result = new ArrayList<>();ApiKey apiKey = new ApiKey("Authorization", "Authorization", "header");result.add(apiKey);return result;}private List<SecurityContext> securityContexts() {//设置需要登录认证的路径List<SecurityContext> result = new ArrayList<>();result.add(getContextByPath("/brand/.*"));return result;}private SecurityContext getContextByPath(String pathRegex) {return SecurityContext.builder().securityReferences(defaultAuth()).forPaths(PathSelectors.regex(pathRegex)).build();}private List<SecurityReference> defaultAuth() {List<SecurityReference> result = new ArrayList<>();AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];authorizationScopes[0] = authorizationScope;result.add(new SecurityReference("Authorization", authorizationScopes));return result;}/*** springboot2.6就算配了ant_path_matcher也会和springfox冲突* 解决springboot2.6 和springfox不兼容问题  Failed to start bean ‘ documentationPluginsBootstrapper ‘ ; nested exception…**/@Beanpublic static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {return new BeanPostProcessor() {@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {customizeSpringfoxHandlerMappings(getHandlerMappings(bean));}return bean;}private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {List<T> copy = mappings.stream().filter(mapping -> mapping.getPatternParser() == null).collect(Collectors.toList());mappings.clear();mappings.addAll(copy);}@SuppressWarnings("unchecked")private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {try {Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");field.setAccessible(true);return (List<RequestMappingInfoHandlerMapping>) field.get(bean);} catch (IllegalArgumentException | IllegalAccessException e) {throw new IllegalStateException(e);}}};}
}

上面配置中有个接口

public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor()

springboot2.6以上版本需要配置中的这个Bean,很重要,务必加上

认证与授权流程演示

运行项目,访问API

Swagger3以上的版本访问的地址是:http://localhost:8080/swagger-ui/index.html

在这里插入图片描述

未登录时访问接口

在这里插入图片描述

登录后访问接口

进行登录操作:登录帐号test 123456

在这里插入图片描述
在这里插入图片描述
点击Authorize按钮,在弹框中输入登录接口中获取到的token信息
在这里插入图片描述
加上接口的注解,与表中的数据对应
在这里插入图片描述
在这里插入图片描述

继续访问
在这里插入图片描述
由于test帐号并没有设置任何权限,所以他无法访问具有pms:brand:read权限的获取品牌列表接口。
在这里插入图片描述

改用其他有权限的帐号登录

改用admin 123456登录后访问,点击Authorize按钮打开弹框,点击logout登出后再重新输入新token

在这里插入图片描述
可以看到有数据了

总结

上面的案例是在某个接口判断的权限,但这个不能动态配置和校验。最好的方式是根据访问的url进行权限校验是最好的,这样就不需要在各个接口加注解了。此方式会单独写个教程进行讲解

源码

链接: 整合SpringSecurity和JWT实现认证和授权

相关文章:

  • 聊聊logback的ThrowableProxyConverter
  • prometheus基础,结合node_exporter监控节点
  • 【数组】-Lc15-三数之和(排序+for循环+滑动窗口)
  • 详细学习Pyqt5的10种容器(Containers)
  • 【自动化测试】pytest 用例执行中print日志实时输出
  • WEBAPI返回图片显示在VUE前端
  • 设置随机种子保证网络可复现性
  • JAVA代码优化:Spring中redis的工具类
  • Java Web——动态Web开发核心-Servlet
  • 短线买入卖出有哪些交易技巧?
  • 使用 Mybatis 的 TypeHandler 存取 Postgresql jsonb 类型
  • 固态硬盘与机械硬盘的区别
  • Java的多态性
  • 栈和队列的OJ题——14.用栈实现队列
  • 区块链媒体:Web3.015个方法解析-华媒舍
  • 实现windows 窗体的自己画,网上摘抄的,学习了
  • 《微软的软件测试之道》成书始末、出版宣告、补充致谢名单及相关信息
  • 【译】React性能工程(下) -- 深入研究React性能调试
  • Git初体验
  • PHP 7 修改了什么呢 -- 2
  • Python实现BT种子转化为磁力链接【实战】
  • Python学习之路16-使用API
  • 阿里云Kubernetes容器服务上体验Knative
  • 函数式编程与面向对象编程[4]:Scala的类型关联Type Alias
  • 离散点最小(凸)包围边界查找
  • 爬虫进阶 -- 神级程序员:让你的爬虫就像人类的用户行为!
  • 数组大概知多少
  • 线上 python http server profile 实践
  • 用 Swift 编写面向协议的视图
  • 整理一些计算机基础知识!
  • ​学习一下,什么是预包装食品?​
  • ​油烟净化器电源安全,保障健康餐饮生活
  • #QT(串口助手-界面)
  • (2)Java 简介
  • (NO.00004)iOS实现打砖块游戏(九):游戏中小球与反弹棒的碰撞
  • (附源码)计算机毕业设计ssm基于Internet快递柜管理系统
  • (黑客游戏)HackTheGame1.21 过关攻略
  • (七)理解angular中的module和injector,即依赖注入
  • (切换多语言)vantUI+vue-i18n进行国际化配置及新增没有的语言包
  • (转) 深度模型优化性能 调参
  • (转)jQuery 基础
  • .net core 6 redis操作类
  • .NET/C# 的字符串暂存池
  • .NET的数据绑定
  • .Net调用Java编写的WebServices返回值为Null的解决方法(SoapUI工具测试有返回值)
  • .NET业务框架的构建
  • .NET运行机制
  • .NET中使用Redis (二)
  • @KafkaListener注解详解(一)| 常用参数详解
  • [383] 赎金信 js
  • [android] 练习PopupWindow实现对话框
  • [AR Foundation] 人脸检测的流程
  • [C/C++]数据结构----顺序表的实现(增删查改)
  • [Cocoa]iOS 开发者账户,联机调试,发布应用事宜
  • [Codeforces] combinatorics (R1600) Part.2