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实现认证和授权

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/193944.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis--14--BigKey 和 热点Key

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 BigKey1.什么是bigkey2.bigkey的危害3.发现bigkeyscan 4.解决bigkey 什么是热点Key&#xff1f;该如何解决1. 产生原因和危害原因危害 2.发现热点key预估发现客户端…

Stable Diffusion AI绘画系列【11】:超萌的Q版手办萌宠系列

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

送女朋友一个猜数字小游戏,猜对了会显示爱心(给你心爱的他或她一个惊喜)

起因是我在学习C语言完成老师布置C语言写一个猜数字的作业&#xff0c;突发奇想&#xff0c;能不能在这个猜对了之后弹出一个不一样的页面&#xff0c;然后就试试看能不能实现。基本思路是这样的&#xff1a; 1&#xff1a;先写一个C语言的猜数字的小游戏&#xff0c;在我上个文…

StackGres 1.6 数据库平台工程功能介绍以及快速上手

StackGres 1.6 数据库平台工程功能 声明式 K8S CRs StackGres operator 完全由 Kubernetes 自定义资源管理。除了 kubectl 或任何其他 Kubernetes API 访问之外&#xff0c;不需要安装任何客户端或其他工具来管理 StackGres。您的请求由 CRD 的 spec 部分表示&#xff0c;任何 …

Redis 数据结构详解

分类 编程技术 Redis 数据类型分为&#xff1a;字符串类型、散列类型、列表类型、集合类型、有序集合类型。 Redis 这么火&#xff0c;它运行有多块&#xff1f;一台普通的笔记本电脑&#xff0c;可以在1秒钟内完成十万次的读写操作。 原子操作&#xff1a;最小的操作单位&a…

MySql下载和安装

MySql下载和安装 一、概述 MySQL是一个开放源代码的关系型数据库管理系统 &#xff0c;由瑞典MySQL AB&#xff08;创始人Michael Widenius&#xff09;公 司1995年开发&#xff0c;迅速成为开源数据库的 No.1。 二、下载和安装 下载地址&#xff1a;https://dev.mysql.com…

Python编程技巧 – 迭代器(Iterator)

Python编程技巧 – 迭代器(Iterator) By JacksonML Iterator(迭代器)是Python语言的核心概念之一。它常常与装饰器和生成器一道被人们提及&#xff0c;也是所有Python书籍需要涉及的部分。 本文简要介绍迭代器的功能以及实际的案例&#xff0c;希望对广大读者和学生有所帮助。…

Xshell会话文件解密获取密码

Xshell会话文件解密获取密码 开发了一个小工具用于获取已存储的xshell会话密码功能简介截图展示下载地址 开发了一个小工具用于获取已存储的xshell会话密码 在日常开发中&#xff0c;服务器太多&#xff0c;密码记不住。使用xshell管理服务器会话&#xff0c;记住密码&#xf…

Linux网络之连接跟踪 conntrack

一 Linux网络之连接跟踪 conntrack k8s 有关conntrack的分析 ① 什么是连接跟踪 netfilter连接跟踪 conntrack 详述 思考&#xff1a;连接跟踪模块会对哪些协议进行跟踪?TCP、UDP、ICMP、DCCP、SCTP、GRE ② 为什么需要连接跟踪 没有连接跟踪有很多问题是不好解决的&a…

牛客在线编程(SQL大厂面试真题)

1.各个视频的平均完播率_牛客题霸_牛客网 ROP TABLE IF EXISTS tb_user_video_log, tb_video_info; CREATE TABLE tb_user_video_log (id INT PRIMARY KEY AUTO_INCREMENT COMMENT 自增ID,uid INT NOT NULL COMMENT 用户ID,video_id INT NOT NULL COMMENT 视频ID,start_time d…

最大乘积分解(动态规划)

相较于我上一题写的动态规划&#xff0c;这一题比较简单 代码如下&#xff1a; #include<stdio.h>int main(void) {long long n, max[101] {0, 1};scanf("%lld", &n);for(int i 1; i < n; i)max[i] i;for(int i 1; i < n; i)for(int j 1; j &…

springboot数据源配置

springboot数据源配置 数据层解决方案——持久化技术 内置持久化解决方案——jdbcTemplate 内置数据库 H2一般用于测试环境&#xff0c;配置profiels&#xff0c;只在开发阶段使用&#xff0c;让他在上线的时候不走这里就可以了 要使用内嵌的数据库H2,要先导入jar包

设置WPF启动画面

WPF启动时间比较长&#xff0c;总让人觉得程序好像没有启动起来&#xff0c;所以想设置一个启动画面 发现WPF设置启动画面竟然如此的简单 第一步将图片放置在主工程目录下&#xff0c;如下图 第二步 将图片生成属性设置为SplashScreen即可 第三步 启动项目你就看到效果了

MDETR 论文报告

MDETR - Modulated Detection for End-to-End Multi-Modal Understanding MDETR - Modulated Detection for End-to-End Multi-Modal Understanding发现问题主要贡献和创新点主要方法和技术MDETR 的架构损失函数1. 框预测损失2. 软标记预测损失3. 对比对齐损失4. 总损失 实验和…

充电桩新老国标兼容性分析

1、背景介绍 1.1、充电桩相关标准发展历程 1.2、兼容性分析历史 1.3、兼容性分析的目的 1.4、兼容性分析的内容 2、B类协议兼容性分析 2.1、协议分层结构 2.2、链路层分析 2.3、版本协商与链路检测 ## 2.4、传输层分析 2.5、应用层 2.5.1、应用层数据 2.5.2、应用层数据…

链表【1】

文章目录 &#x1f348;2. 两数相加&#x1f34c;1. 题目&#x1f34f;2. 算法原理&#x1f353;3. 代码实现 &#x1f349;445. 两数相加 II&#x1f34d;1. 题目&#x1f350;2. 算法原理&#x1fad0;3. 代码实现 &#x1f348;2. 两数相加 &#x1f34c;1. 题目 题目链接&…

springboot整合easy-es实现数据的增删改查

背景 目前公司的一个老项目&#xff0c;查询贼慢&#xff0c;需要想办法提升一下速度&#xff0c;于是就想到了ES&#xff0c;现在尝试一下将ES整合到项目中来提升检索效率。 ES是基于倒排索引实现的&#xff0c;倒排索引中一个表相当于一个索引&#xff0c;表中的每条记录都…

【【Micro Blaze 的 最后补充 与 回顾 】】

Micro Blaze 的 最后补充 与 回顾 Micro Blaze 最小系统 以 MicroBlaze 为核心、LocalMemory&#xff08;片上存储&#xff09;为内存&#xff0c;加上传输信息使用的 UART串口就构成了嵌入式最小系统。当程序比较简单时&#xff0c;Local Memory 可以作为程序的运行空间以及…

VUE语法--img图片不显示/img的src动态赋值图片显示

1、问题概述 常见情景1&#xff1a;在VUE中使用img显示图片的时候&#xff0c;通过传参的方式传入图片的路径和名称&#xff0c;VUE不加载本地资源而是通过http://localhost:8080/...的地址去加载网络资源&#xff0c;从而出现了图片无法显示的情况。 常见情景2&#xff1a;针…

python装饰器解析(关键点:高阶函数、嵌套函数)(参数化装饰器、类装饰器)

文章目录 Python装饰器解析什么是Python装饰器基础理解 如何创建装饰器&#xff08;关键点&#xff1a;高阶函数、嵌套函数&#xff09;创建基础装饰器 使用装饰器使用示例 装饰器的返回值参数化装饰器创建参数化装饰器语法示例使用示例 类装饰器创建类装饰器语法示例使用示例 …