石一歌的Shiro笔记
Shiro
概念

- Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
- SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
- Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。
springboot整合shiro
对密码部分进行了增强,使用随机值加盐加密
pom
org.apache.shiro
shiro-spring-boot-web-starter
1.6.0
LoginController
@RequestMapping("/login")
public String login(String username, String password, Model model) {
//获得主体
Subject subject = SecurityUtils.getSubject();
//获取token并进行登录及异常处理
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
return "index";
} catch (UnknownAccountException e) {
model.addAttribute("msg", "用户名错误!");
return "login";
} catch (IncorrectCredentialsException e) {
model.addAttribute("msg", "密码错误!");
return "login";
}
}
UserRealm
在用户域进行授权和认证
public class UserRealm extends AuthorizingRealm {
@Resource
UserService userService;
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 1.从主体传过来的认证信息中,获得用户名
String username = (String) authenticationToken.getPrincipal();
// 2.通过用户名到数据库中获取凭证
User user = userService.getUserByUserName(userName);
if (user == null) {
return null;
}
return new SimpleAuthenticationInfo(user,//用户/用户名
user.getPassword(),//数据库拿到的密码
ByteSource.Util.bytes(user.getSalt()),//salt
this.getName());//RealmName
}
//授权(已通过认证)
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
User user = (User) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
for (SysRole role : user.getRoles()) {
info.addRole(role.getName());
for (SysPermission permission : role.getPermissions()) {
info.addStringPermission(permission.getName());
}
}
return info;
}
}
ShiroConfig
@Configuration
public class ShrioConfig {
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
//添加 Shiro 的内置过滤器
/*
anon : 无需认证,就可以访问
authc : 必须认证,才能访问
user : 必须拥有 "记住我"功能才能用
perms : 拥有对某个资源的权限才能访问
role : 拥有某个角色权限才能访问
有顺序问题,/**通常在最下面
*/
Map map = new LinkedHashMap();
//此处的过滤器可以使用shiro相应的注解实现
map.put("/static/**", "anon");
map.put("/logout", "logout");
map.put("/login", "anon")
map.put("/**", "authc");
bean.setFilterChainDefinitionMap(map);
//设置登录页
bean.setLoginUrl("/toLogin");
//设置首页
bean.setSuccessUrl("/index");
//设置未授权页面
bean.setUnauthorizedUrl("/403");
return bean;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//关联用户域
return manager.setRealm(userRealm);
}
@Bean(name = "userRealm")
public UserRealm userRealm(@Qualifier("credentialsMatcher")
HashedCredentialsMatcher hashedCredentialsMatcher) {
UserRealm userRealm = new UserRealm();
//关联凭证匹配器
return userRealm.setCredentialsMatcher(hashedCredentialsMatcher);
}
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 散列算法
hashedCredentialsMatcher.setHashIterations(2); // 散列次数
return hashedCredentialsMatcher;
}
}
权限控制注解(在controller方法上使用)

springboot整合shiro+jwt
pom
com.auth0
java-jwt
3.4.0
LoginController
验证账号密码无误,会返回Token,其他请求需要携带Token访问
@PostMapping("/login")
public ResultMap login(@RequestParam("username") String username,
@RequestParam("password") String password) {
String realPassword = userMapper.getPassword(username);
if (realPassword == null) {
return resultMap.fail().code(401).message("用户名错误");
} else if (!realPassword.equals(password)) {
return resultMap.fail().code(401).message("密码错误");
} else {
return resultMap.success().code(200).message(JWTUtil.createToken(username));
}
}
JWTUtil
创建jwt工具类,方便使用,包括生成token验证token以及获取token信息方法
public class JWTUtil {
// 过期时间 24 小时
private static final long EXPIRE_TIME = 60 * 24 * 60 * 1000;
// 密钥
private static final String SECRET = "SHIRO+JWT";
/**
* 生成 token
*
* @param username 用户名
* @return 加密的token
*/
public static String createToken(String username) {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
// 附带username信息
return JWT.create()
.withClaim("username", username)
//到期时间
.withExpiresAt(date)
//创建一个新的JWT,并使用给定的算法进行标记
.sign(algorithm);
} catch (UnsupportedEncodingException e) {
return null;
}
}
/**
* 校验 token 是否正确
*
* @param token 密钥
* @param username 用户名
* @return 是否正确
*/
public static boolean verify(String token, String username) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
//在token中附带了username信息
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
//验证 token
verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 获得token中的信息,无需secret解密也能获得
*
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
JWTFilter
自定义过滤器
检验请求头是否带有 token
((HttpServletRequest) request).getHeader("Token") != null如果带有 token,执行 shiro 的 login() 方法,将 token 提交到 Realm 中进行检验;如果没有 token,说明当前状态为游客状态
如果在 token 校验的过程中出现错误,则重定向到
/unauthorized/**
public class JWTFilter extends BasicHttpAuthenticationFilter {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 如果带有 token,则对 token 进行检查,否则直接通过
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
//判断请求的请求头是否带上 "Token"
if (isLoginAttempt(request, response)) {
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
//token 错误
responseError(response, e.getMessage());
}
}
//如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
return true;
}
/**
* 判断用户是否想要登入。
* 检测 header 里面是否包含 Token 字段
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("Token");
return token != null;
}
/**
* 执行登陆操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Token");
JWTToken jwtToken = new JWTToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 将非法请求跳转到 /unauthorized/**
*/
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());
}
}
}
UserRealm
public class UserRealm extends AuthorizingRealm {
@Resource
UserService userService;
/**
* 重写supports方法,使之能识别JWTToken
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 已经用JWTToken代替了原有token,所以这里获取的凭据可以用JWTUtil解密
String token = (String) authenticationToken.getCredentials();
// 解密获得username,用于和数据库进行对比
String username = JWTUtil.getUsername(token);
if (username == null || !JWTUtil.verify(token, username)) {
throw new AuthenticationException("token认证失败!");
}
String password = userService.getPassword(username);
if (password == null) {
throw new AuthenticationException("该用户不存在!");
}
return new SimpleAuthenticationInfo(username, token, this.getName());
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = JWTUtil.getUsername(principals.toString());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
User user = userService.getUserByUserName(userName);
for (SysRole role : user.getRoles()) {
info.addRole(role.getName());
for (SysPermission permission : role.getPermissions()) {
info.addStringPermission(permission.getName());
}
}
return info;
}
}
ShiroConfig
更改ShiroConfig中getShiroFilterFactoryBean方法的内容,使用JWTFilter
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器并且取名为jwt
Map filterMap = new LinkedHashMap<>();
//设置我们自定义的JWT过滤器
filterMap.put("jwt", new JWTFilter());
bean.setFilters(filterMap);
Map map = new HashMap<>();
// 所有请求通过我们自己的JWT Filter
map.put("/**", "jwt");
// 访问 /unauthorized/** 不通过JWTFilter
map.put("/unauthorized/**", "anon");
bean.setFilterChainDefinitionMap(map);
bean.setSecurityManager(securityManager);
// 设置无权限时跳转的 url;
bean.setUnauthorizedUrl("/unauthorized/无权限");
return bean;
}
权限控制注解
与上一部分相同
springboot整合shiro+jwt(fastdep)
pom
com.louislivi.fastdep
fastdep-shiro-jwt
1.0.7
yaml
fastdep:
shiro-jwt:
filter: #shiro过滤规则
admin:
path: /admin/**
role: jwt # jwt为需要进行token校验
front:
path: /front/**/**
role: anon # anon为无需校验
secret: "6Dx8SIuaHXJYnpsG18SSpjPs50lZcT52" # jwt秘钥
# expireTime: 7200000 # token有效期
# prefix: "Bearer " # token校验时的前缀
# signPrefix: "Bearer " # token生成签名的前缀
# header: "Authorization" # token校验时的header头
# 以下对应为shiro配置参数,无特殊需求无需配置
# loginUrl:
# successUrl:
# unauthorizedUrl:
# filterChainDefinitions:
具体应用参考官方文档
参考链接
- Shiro安全框架【快速入门】就这一篇
- 教你 Shiro 整合 SpringBoot,避开各种坑
- 教你 Shiro + SpringBoot 整合 JWT
- fastdep-shiro-jwt