SrpingBoot-Shrio
整合 JWT
(2019.12.23)
JSON Web Token(JWT)
是一个非常轻巧的规范。这个规范允许我们使用JWT
在用户和服务器之间传递安全可靠的信息。
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
一个JWT
实际上就是一个字符串,它由三部分组成: 头部、载荷与签名。
1. 引入JWT
依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>spring-boot-demo-shiro-base</groupId>
<artifactId>spring-boot-demo-shiro-base</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-demo-shiro6</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<mybatis.version>1.3.2</mybatis.version>
<mysql.version>8.0.12</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!--使用阿里巴巴的德鲁伊作为数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mysql数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--Shiro 依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.4.0-RC2</version>
</dependency>
<!--jjwt 依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
<exclusions>
<exclusion>
<artifactId>shiro-core</artifactId>
<groupId>org.apache.shiro</groupId>
</exclusion>
</exclusions>
</dependency>
<!--html页面使用shiro标签依赖-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!-- 对象池,使用redis时必须引入 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
2.application.yml
加上jwt
配置信息
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
#数据源其他配置
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
redis:
host: localhost
# 连接超时时间(记得添加单位,Duration)
timeout: 10000ms
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-wait: -1ms
# 连接池中的最大空闲连接 默认 8
max-idle: 8
# 连接池中的最小空闲连接 默认 0
min-idle: 0
#jwt 配置
jwt:
config:
key: zhihao #加密密匙
# token有效期,单位秒
jwtTimeOut: 3600
# 后端免认证接口 url
anonUrl: /login,
mybatis:
mapper-locations: mapper/*.xml
type-aliases-package: com.zhihao.entity
3.创建jwtUtil.java
工具类,进行签发和解析token
@Component
@ConfigurationProperties(prefix = "jwt.config")
public class JwtUtil {
@Value("${jwt.config.key}")
private String key;
private long jwtTimeout;//一个小时
private String anonUrl;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getJwtTimeout() {
return jwtTimeout;
}
public void setJwtTimeout(long jwtTimeout) {
this.jwtTimeout = jwtTimeout;
}
public String getAnonUrl() {
return anonUrl;
}
public void setAnonUrl(String anonUrl) {
this.anonUrl = anonUrl;
}
/**
* 生成JWT
*
* @param id 用户id
* @param subject 用户名
* @return java.lang.String
*/
public String createJWT(String id, String subject) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder()
.setId(id) //id
.setSubject(subject) //主题
.setIssuedAt(now) //签发时间
.signWith(SignatureAlgorithm.HS256, key); //加密
//超时大于0 设置token超时
if (jwtTimeout > 0) {
//转换成超时毫秒
long timeout = nowMillis + (jwtTimeout * 1000);
builder.setExpiration(new Date(timeout));
}
return builder.compact();
}
/**
* 解析JWT
*
* @param jwtStr
* @return
*/
public Claims parseJWT(String jwtStr){
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody();
}
}
4.自定义自己的过滤器JWTFilter
我们使用的是 shiro
默认的权限拦截 Filter
,而因为JWT
的整合,我们需要自定义自己的过滤器 JWTFilter,JWTFilter
继承了 BasicHttpAuthenticationFilter
,并部分原方法进行了重写。
- 检验请求头是否带有 token
((HttpServletRequest) request).getHeader("Token") != null
- 如果带有 token,执行 shiro 的 login() 方法,将 token 提交到 Realm 中进行检验;如果没有 token,说明当前状态为游客状态(或者其他一些不需要进行认证的接口)
public class JWTFilter extends BasicHttpAuthenticationFilter {
private Logger log = LoggerFactory.getLogger(this.getClass());
private static final String TOKEN = "Token";
private AntPathMatcher pathMatcher = new AntPathMatcher();
private Map errorMap;
@SneakyThrows
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
JwtUtil JwtUtil = context.getBean(JwtUtil.class);
//判断是否是登录请求
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String[] split = StringUtils.split(JwtUtil.getAnonUrl(), ",");
for (String url : split) {
//如果是后端免认证接口 直接放行
if (pathMatcher.match(url,httpServletRequest.getRequestURI())){
return true;
}
}
//进入executeLogin方法判断请求的请求头是否带上 "Token"
if (isLoginAttempt(request, response)) {
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
return executeLogin(request, response);
}else {
this.tokenError(response,"token为空");
}
return false;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(TOKEN);
try {
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(new JWTToken(token));
// 如果没有抛出异常则代表登入成功,返回true
return true;
} catch (Exception e) {
this.tokenError(response, "token认证失败");
return false;
}
}
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader(TOKEN);
return token != null;
}
/**
* token问题响应
*
* @param response
* @param msg
* @return void
* @author: zhihao
* @date: 2019/12/24
* {@link #}
*/
private void tokenError(ServletResponse response,String msg) throws IOException {
errorMap = new LinkedHashMap();
errorMap.put("code", "error");
errorMap.put("msg", msg);
//响应token为空
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.resetBuffer(); //清空第一次流响应的内容
//转成json格式
ObjectMapper object = new ObjectMapper();
String asString = object.writeValueAsString(errorMap);
response.getWriter().println(asString);
}
/**
* 对跨域提供支持
*/
@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);
}
}
5, JWTToken.java
@Data
public class JWTToken implements AuthenticationToken {
private String token;
public JWTToken(String token) {
this.token = token;
}
public JWTToken() {
}
@Override
public Object getPrincipal() {
return this.token;
}
@Override
public Object getCredentials() {
return this.token;
}
}
6. 配置ShiroConfig
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 在 Shiro过滤器链上加入 JWTFilter
LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
filters.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filters);
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//所有url都必须认证通过jwt过滤器才可以访问
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public DefaultWebSecurityManager securityManager() {
// 配置SecurityManager,并注入shiroRealm
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
//设置缓存管理器 省略..
return securityManager;
}
@Bean
public ShiroRealm shiroRealm() {
// 配置Realm,需自己实现
ShiroRealm shiroRealm = new ShiroRealm();
return shiroRealm;
}
/**
* 开启shiro认证注解
*
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
7. 配置自己实现的ShiroRealm
public class ShiroRealm extends AuthorizingRealm {
/**
* 支持自定义认证令牌
*
* @param token
* @return boolean
* @author: zhihao
* @date: 2019/12/24
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
@Autowired
private JwtUtil jwtUtil;
/**
* 授权模块>>>>获取用户角色和权限
*
* @param principal
* @return org.apache.shiro.authz.AuthorizationInfo
* @author: zhihao
* @date: 2019/12/13
* {@link #}
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
User user = (User) SecurityUtils.getSubject().getPrincipal();
String username = user.getUsername();
//创建授权对象进行封装角色和权限信息进去进行返回 注意不是SimpleAuthenticationInfo
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获取用户角色集
List<Role> roleList = roleService.findRoleByUserName(username);
Set<String> roleSet = new HashSet<>();
for (Role role : roleList) {
roleSet.add(role.getMemo());
}
System.out.println("用户拥有的角色>>>"+roleSet);
//添加角色进角色授权
info.setRoles(roleSet);
List<Permission> permissionList = permissionService.findPermissionByUserName(username);
Set<String> permissionSet = new HashSet<>();
for (Permission permission : permissionList) {
permissionSet.add(permission.getName());
}
System.out.println("用户拥有的权限>>>"+permissionSet);
//添加权限进权限授权
info.setStringPermissions(permissionSet);
return info;
}
/**
* 用户认证
*
* @param authenticationToken 身份认证 token
* @return AuthenticationInfo 身份认证信息
* @throws AuthenticationException 认证相关异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
// 这里的 token是从 JWTFilter 的 executeLogin 方法传递过来的
String token = (String) authenticationToken.getCredentials();
String username = null;
try {
username = jwtUtil.parseJWT(token).getSubject();
} catch (Exception e) {
//抛出token认证失败
throw new AuthenticationException("token认证失败");
}
// 通过用户名到数据库查询用户信息
User user = userService.findUserByName(username);
if (user == null) {
throw new UnknownAccountException("用户不存在!");
}
if (user.getStatus().equals("0")) {
throw new LockedAccountException("账号已被锁定,请联系管理员!");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, token, getName());
return info;
}
}
7. 配置权限不足异常处理器
/**
* @Author: zhihao
* @Date: 2019/12/24 12:57
* @Description: 登录相关异常处理
* @Versions 1.0
**/
@ControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class loginExceptionHandler {
private Logger log = LoggerFactory.getLogger(this.getClass());
private Map exceptionMap;
/**
* 拦截权限不足异常,并响应
*
* @param e
* @return java.util.Map
* @author: zhihao
* @date: 2019/12/24
*/
@ExceptionHandler(value = UnauthorizedException.class)
@ResponseBody
public Map unauthorizedException(UnauthorizedException e){
exceptionMap = new LinkedHashMap();
exceptionMap.put("msg", e.getMessage());
log.error(e.getMessage());
return exceptionMap;
}
}
8.登录接口,校验成功签发token
/**
* 登录免认证 登录成功签发token
*
* @param username 用户名
* @param password 密码
* @return java.util.Map 简陋的结果包装
* @author: zhihao
* @date: 2019/12/12
*/
@PostMapping("/login")
public Map login(@RequestParam("username") String username,@RequestParam("password") String password){
resultMap = new LinkedHashMap<>();
// 密码加密
String md5 = new SimpleHash("MD5", password, username, 1024).toString();
User user = userService.findUserByName(username);
if(user != null && md5.equals(user.getPassword())){
resultMap.put("code", "success");
resultMap.put("token", jwtUtil.createJWT(user.getId(), user.getUsername()));
return resultMap;
}
resultMap.put("code","error");
resultMap.put("msg","用户不存在或者密码错误");
return resultMap;
}