1.概述
快速集成用户权限,利用jwt做无状态登录,swagger2做api文档
代码参考
shiro
Shiro以Shiro开发团队所谓的“应用程序安全性的四个基石”为目标-身份验证,授权,会话管理和密码学:
身份验证(Authentication
):有时称为“登录”,这是证明用户是他们所说的身份的行为。
授权(Authorization
):访问控制的过程,即确定“谁”有权访问“什么”。
会话管理(Session Management
):即使在非Web或EJB应用程序中,也可以管理用户特定的会话。
密码学(Cryptography
):使用密码算法保持数据安全,同时仍然易于使用。
mybatisplus
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,
为简化开发、提高效率而生。
详细见:SpringBoot之【mybatisplus】快速上手
jwt(JSON Web Token)
授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单一登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
信息交换:JSON Web令牌是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确定发件人是他们所说的人。另外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。
2.搭建流程
2.1 依赖引入
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<!--代码生成器 依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<!--模板引擎 依赖-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
<!--集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.2</version>
</dependency>
<!--JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
2.2 代码生成
代码生成工具
public class mpGen {
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
final String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/shiro_jwt/src/main/java");
gc.setAuthor("jiawei");
gc.setOpen(false);
gc.setIdType(IdType.AUTO);
gc.setSwagger2(true); //实体属性 Swagger2 注解
gc.setFileOverride(true); //设置是否覆盖原来的代码 最好设置为false 或者 另外配置路径
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/shiro?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
mpg.setDataSource(dsc);
// 包配置
final PackageConfig pc = new PackageConfig();
//pc.setModuleName(scanner("模块名"));
pc.setParent("com.example.shiro_jwt");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
mpg.setTemplate(new TemplateConfig().setXml(null) //设置为null,调整mapper的xml至resources下,
// .setController("...");
// .setEntity("...");
// .setMapper("...");
// .setXml("...");
// .setService("...");
// .setServiceImpl("...");
);
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/shiro_jwt/src/main/resources/com/example/mpjwt/mapper/"
+ tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
StrategyConfig strategy = new StrategyConfig();
// 表名生成策略(下划线转驼峰命名)
strategy.setNaming(NamingStrategy.underline_to_camel);
// 列名生成策略(下划线转驼峰命名)
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
// 自定义实体父类
//strategy.setSuperEntityClass("com.baomidou.ant.common.BaseEntity");
// 是否启动Lombok配置
strategy.setEntityLombokModel(true);
// 是否启动REST风格配置
strategy.setRestControllerStyle(true);
//strategy.setInclude("ums_admin");
// 写于父类中的公共字段 父类没有id就注释掉,否则实体类不生成 id属性
//strategy.setSuperEntityColumns("id");
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
2.3 yml设置
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
#schema: classpath:shiro.sql #后期注释掉,否则每次启动都会初始化sql,大坑啊
#data: classpath:db/data.sql #后期注释掉,否则每次启动都会初始化sql,大坑啊
initialization-mode: always #不加的话不会执行schema和data
jedis:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 8 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
timeout: 3000ms # 连接超时时间(毫秒)
mybatis:
mapper-locations:
- classpath:mapper/*.xml
- classpath*:com/**/mapper/*.xml
2.4 新建myrelm域
@Autowired
private IUserService userService;
@Autowired
private IRoleService roleService;
@Autowired
private IRolePermissionService iRolePermissionService;
@Autowired
private IPermissionService iPermissionService;
/**
* 必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token != null && token instanceof JwtToken;
}
/**
* 只有当需要检测用户权限的时候才会调用此方法
* 将权限和角色绑定返回
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = JwtUtil.getUsername(principals.toString());
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("username",username);
User user = userService.getOne(userQueryWrapper);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
Role role = roleService.getById(user.getRoleId());
Set<String> roleSet = new HashSet<>();// 放置角色,我这里只设计成单用户单角色,简单点
Set<String> permissionSet = new HashSet<>();//放置权限
roleSet.add(role.getRoleName());
QueryWrapper<RolePermission> iRolePermissionServiceQueryWrapper = new QueryWrapper<>();
iRolePermissionServiceQueryWrapper.eq("role_id",user.getRoleId());
List<RolePermission> rolePermissionList = iRolePermissionService.list(iRolePermissionServiceQueryWrapper);
for (RolePermission rolePermission : rolePermissionList) {
Permission permission = iPermissionService.getById(rolePermission.getPermissionId());
permissionSet.add(permission.getName());
}
simpleAuthorizationInfo.setRoles(roleSet);
simpleAuthorizationInfo.setStringPermissions(permissionSet);
return simpleAuthorizationInfo;
}
/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
// 解密获得username,用于和数据库进行对比
String username = JwtUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("token无效");
}
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("username",username);
User userBean = this.userService.getOne(userQueryWrapper);
if (userBean == null) {
throw new UnknownAccountException("用户不存在!");
}
if (!JwtUtil.verify(token, username, userBean.getPassword())) {
throw new IncorrectCredentialsException ("用户名或密码错误");
}
/*LockedAccountException 用户已锁定异常*/
return new SimpleAuthenticationInfo(token,token,getName());
}
2.5 新建jwttoken
package com.example.shiro_jwt.myentity;
import org.apache.shiro.authc.AuthenticationToken;
/**
*
*/
public class JwtToken implements AuthenticationToken {
private String token;
public String getToken() {
return token;
}
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
2.6 新建shiroConfig
package com.example.shiro_jwt.config;
import com.example.shiro_jwt.mapper.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/user/login/**", "anon");
filterChainDefinitionMap.put("/user/logout/**", "anon");
filterChainDefinitionMap.put("/**.js", "anon");
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/swagger**/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/unauthorized/**", "anon");
// filterChainDefinitionMap.put("/user/**", "authc");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized/403");//未授权界面;
shiroFilterFactoryBean.setLoginUrl("/toLogin"); //未登录跳转页面
shiroFilterFactoryBean.setSuccessUrl("/success");//登录成功跳转界面
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean("securityManager")
public SecurityManager securityManager(MyRealm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 添加注解支持
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
2.7 swagger2支持
package com.example.shiro_jwt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
/**
* Swagger2API文档的配置
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi(){
ParameterBuilder ticketPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<Parameter>();
ticketPar.name("Authorization").description("Authorization")
.modelRef(new ModelRef("string")).parameterType("header")
.required(false).build(); //header中的ticket参数非必填,传空也可以
pars.add(ticketPar.build()); //根据每个方法名也知道当前方法在设置什么参数
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//为当前包下controller生成API文档
.apis(RequestHandlerSelectors.basePackage("com.example.shiro_jwt.controller"))
//为有@Api注解的Controller生成API文档
// .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
//为有@ApiOperation注解的方法生成API文档
// .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build()
//添加登录认证
.globalOperationParameters(pars)
;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("SwaggerUI演示")
.description("")
.contact(new Contact("jiawei","","admin@ff.com"))
.version("1.0")
.build();
}
}
2.8 JwtFilter 过滤请求
package com.example.shiro_jwt.config;
import com.example.shiro_jwt.myentity.JwtToken;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
public class JwtFilter extends BasicHttpAuthenticationFilter {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(isLoginAttempt(request,response)){
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
responseError(response, e.getMessage());
}
}
return true;
}
/**
* 判断用户是否想要登入。
* 检测 header 里面是否包含 token 字段
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("Authorization");
return token != null;
}
/**
*执行登陆操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Authorization");
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
try {
getSubject(request, response).login(jwtToken);
} catch (AuthenticationException e) {
logger.error(e.getMessage());
response.getWriter().print(e.getMessage());
}
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
private void responseError(ServletResponse response, String message) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//设置编码,否则中文字符在重定向时会变为空字符串
message = URLEncoder.encode(message, "UTF-8");
httpServletResponse.sendRedirect("/unauthorized/" + message);
} catch (IOException e) {
logger.error(e.getMessage());
}
}
}
userController登录相关
package com.example.shiro_jwt.controller;
import com.example.shiro_jwt.common.api.CommonResult;
import com.example.shiro_jwt.common.api.ResultCode;
import com.example.shiro_jwt.entity.User;
import com.example.shiro_jwt.myentity.JwtToken;
import com.example.shiro_jwt.common.JwtUtil;
import com.example.shiro_jwt.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* <p>
* 前端控制器
* </p>
*
* @author jiawei
* @since 2020-01-08
*/
@RestController
@RequestMapping("/user")
@Api(value = "SpringBoot集成Shiro JWT测试接口", tags = "LoginController")
public class UserController {
@Autowired
private IUserService service;
@ApiOperation(value = "用户登录")
@RequestMapping(value = "/login",method = RequestMethod.POST)
public CommonResult<JwtToken> login(@RequestParam("username") String username, @RequestParam("password") String password){
String token = JwtUtil.sign(username, password);
JwtToken jwtToken=new JwtToken(token);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(jwtToken);
} catch (AuthenticationException e) {
return CommonResult.failed(e.getMessage());
}
return CommonResult.success(jwtToken,"登录成功");
}
@RequiresRoles(logical = Logical.OR, value = {"商品管员"})
@ApiOperation(value = "用户列表查询")
@RequestMapping(value = "/list",method = RequestMethod.GET)
public CommonResult list(){
List<User> list = service.list();
System.out.println(list);
return CommonResult.success(list);
}
@ExceptionHandler(UnauthorizedException.class)
public ResultCode authorzation(Exception ex) {
return ResultCode.FORBIDDEN;// 无权限返回的信息
}
}
3.测试
密码输入错误
用户名输入错误
登录成功返回token