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