前言
Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比 Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
-
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。这不就是我们想要的嘛,而且 Shiro 的 API 也是非常简单;其基本功能点如下图所示:
clipboard.png -
Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
-
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
-
Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
-
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
-
Web Support:Web 支持,可以非常容易的集成到 Web 环境;
-
Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
-
Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Spring Boot 集成 Shiro
pom.xml
<!--Shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
ShiroConfig
/**
* @ClassName ShiroConfig
* @Author zwy
* @Data 1/4/2021 下午4:31
*/
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setFilterChainDefinitionMap((Map<String, String>) new HashMap<>().put("jwtFilter",new JwtFilter()));
// 添加 jwt 专用过滤器,拦截除 /login 和 /logout 外的请求
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwtFilter", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
Map<String, String> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("/doc.html","anon");
//设置不拦截druid
linkedHashMap.put("/druid/**","anon");
//Swagger的所有请求的资源和请求的地址都不需要拦截
linkedHashMap.put("/swagger/**","anon");
linkedHashMap.put("/v2/api-docs","anon");
linkedHashMap.put("/swagger-ui.html","anon");
linkedHashMap.put("/swagger-resources/**","anon");
linkedHashMap.put("/webjars/**","anon");
linkedHashMap.put("/csrf","anon");
linkedHashMap.put("/login","anon");
linkedHashMap.put("/captcha.jpg","anon");
linkedHashMap.put("/email","anon");
linkedHashMap.put("/register","anon");
linkedHashMap.put("/**","jwtFilter,authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(linkedHashMap);
shiroFilterFactoryBean.setSecurityManager(securityManager());
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
Set<Realm> set = new HashSet<>();
set.add(MyRealm());
set.add(TokenRealm());
defaultWebSecurityManager.setRealms(set);
return defaultWebSecurityManager;
}
/**
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* Token 认证
* @return
*/
@Bean
public Realm TokenRealm(){
TokenRealm shiroRealm = new TokenRealm();
return shiroRealm;
}
/**
* 账号认证
* @return
*/
@Bean
public Realm MyRealm(){
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCredentialsMatcher(credentialsMatcher());
return shiroRealm;
}
@Bean
public CredentialsMatcher credentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName(LogicUtil.hashName);
hashedCredentialsMatcher.setHashIterations(LogicUtil.hashIterations);
return hashedCredentialsMatcher;
}
}
TokenRealm(token认证)
/**
* @author lanys
* @Description:
* @date 22/6/2021 下午3:00
*/
public class TokenRealm extends AuthorizingRealm {
@Autowired
private SysMenuServer menuServer;
@Autowired
private SysUserService userService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 管理权限
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//得登录的用户名
SysUser username = (SysUser) principals.getPrimaryPrincipal();
System.out.println(("username:"+username.getUsername()));
Set<String> permissionSet = new HashSet<>();
List<String> permissions;
if (Constants.SUPER_ADMIN.equals(username)){
permissions = menuServer.findPermissionsByRole();
if (permissions != null) {
for (String permission : permissions) {
if (StrUtil.isNotEmpty(permission)) {
permissionSet.add(permission);
}
}
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissionSet);
return info;
}
/**
* 管理登录 如果有异常,则令牌无效
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//得到客户端传过来的令牌
String tokenString =(String) token.getCredentials();
//得到
String principal = (String) token.getPrincipal();
SysUser sysUser = userService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, principal));
//根据令牌得用户名,可以查用户库
if (tokenString==null&&sysUser==null)
{
throw new RuntimeException("登录失败");
}
return new SimpleAuthenticationInfo(sysUser, tokenString, getName());
}
}
ShiroRealm(账号密码认证)
/**
* @ClassName ShiroRealm
* @Author zwy
* @Data 1/4/2021 下午4:37
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private SysUserService userService;
@Autowired
private SysMenuServer menuServer;
/**
* 授权
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("权限授权");
Set<String> permissionSet = new HashSet<>();
List<String> permissions;
SysUser userName = (SysUser) principals.getPrimaryPrincipal();
if (Constants.SUPER_ADMIN.equals(userName.getUsername())){
permissions = menuServer.findPermissionsByRole();
if (permissions != null) {
for (String permission : permissions) {
if (StrUtil.isNotEmpty(permission)) {
permissionSet.add(permission);
}
}
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissionSet);
return info;
}
/**
* 认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
LambdaQueryWrapper<SysUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(SysUser::getUsername,principal);
SysUser user = userService.getOne(lambdaQueryWrapper);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user,user.getPassword(),new Md5Hash(user.getSalt()),getName());
return simpleAuthenticationInfo;
}
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
super.setCredentialsMatcher(credentialsMatcher);
}
}
JwtToken(token认证实体)
/**
* @author lanys
* @Description:
* @date 21/6/2021 下午12:52
*/
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
// 加密后的 JWT token串
private String token;
private String userName;
public JwtToken(String token) {
this.token = token;
this.userName = TokenUtil.getClaimFiled(token, Constants.jwtTokenSecret);
}
@Override
public Object getPrincipal() {
return this.userName;
}
@Override
public Object getCredentials() {
return this.token;
}
}
JwtFilter(token过滤器)
/**
* @author lanys
* @Description:
* @date 21/6/2021 上午11:42
*/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
private String getToken(ServletRequest request, ServletResponse response) {
HttpServletRequest servletRequest = (HttpServletRequest) request;
String authorization = servletRequest.getHeader("Authorization");
if (authorization==null){
return null;
}
String token = authorization.replace("Bearer ", "");
return token;
}
/**
* 前置处理
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
String token = getToken(request, response);
System.out.println(((HttpServletRequest) request).getHeader("Authorization"));
//判断请求的请求头是否带上 "Token"
if (token != null) {
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
try {
executeLogin(request, response);
return true;
} catch (AuthenticationException e) {
//token 错误
return false;
} catch (Exception e) {
return false;
}
}
//如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
return true;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
String token = getToken(request, response);
if (token != null && TokenUtil.isTokenExpired(token) == true) {
try {
log.info("提交UserModularRealmAuthenticator决定由哪个realm执行操作...");
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
} catch (Exception e) {
log.info("捕获到身份认证异常");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(Result.fail("Token失效,请重新登录...").toString());
return false;
}
} else {
log.info("Token失效...");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(Result.fail("Token失效,请重新登录...").toString());
return false;
}
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
HttpServletRequest servletRequest = (HttpServletRequest) request;
String token = servletRequest.getHeader("Authorization");
return new JwtToken(token);
}
}
LoginController(登录)
@ApiOperation(value = "登录")
@ResponseBody
@GetMapping("/login")
public Result login(String username, String password,String captcha) {
// ShiroUtil.getKaptcha(Constants.KAPTCHA_SESSION_KEY,captcha);
if (StringUtils.isAllBlank(username, password,captcha)) {
return Result.fail("数据不能为空");
}
SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password));
String token = TokenUtil.doGenerateToken(SecurityLogic.getName(),SecurityLogic.getPassword());
EhcacheMap.getInstance().put(token,token);
return Result.success().put(token);
}
TokenUtil(生成token&检查是否过期)
/**
* @ClassName TokenUtil
* @Author zwy
* @Data 1/4/2021 下午7:05
*/
@Slf4j
public class TokenUtil {
/**
* 解析
* @param token
* @return
*/
public static Claims getClaimFromToken(String token) {
return Jwts.parser().setSigningKey(Constants.jwtTokenSecret).parseClaimsJws(token).getBody();
}
/**
* 获得token中的自定义信息,无需secret解密也能获得
*/
public static String getClaimFiled(String token, String filed) {
try {
return Jwts.parser().setSigningKey(filed).parseClaimsJws(token).getBody().getSubject();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 获取用户
* @param token
* @return
*/
public static String getName(String token){
return Jwts.parser().setSigningKey(Constants.jwtTokenSecret).parseClaimsJws(token).getBody().getSubject();
}
/**
* 判断过期
*
* @param token
* @return
*/
public static Boolean isTokenExpired(String token) {
try {
final Date expiration = getClaimFromToken(token).getExpiration();
log.info("token到期时间:"+expiration);
return true;
} catch (Exception e) {
log.info("token过期");
return false;
}
}
/**
* 生成
*
* @param username
* @return
*/
public static String doGenerateToken(String username,String password) {
final Date createdDate = new Date();
//Constants.time * 1000
final Date finishDate = new Date(createdDate.getTime() + 86400);
return Jwts.builder()
.setSubject(username)
.setAudience(password)
.setIssuedAt(createdDate)
.setExpiration(finishDate)
.signWith(SignatureAlgorithm.HS256, Constants.jwtTokenSecret).compact();
}
}
测试
Token验证
总结
Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。
Shiro特点:
- 易于理解的 Java Security API;
- 简单的身份认证(登录),支持多种数据源(LDAP,JDBC 等);
- 对角色的简单的签权(访问控制),也支持细粒度的鉴权;
- 支持一级缓存,以提升应用程序的性能;
- 内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;
- 异构客户端会话访问;
- 非常简单的加密 API(Shiro加密后的密码是解析不出来的);
- 不跟任何的框架或者容器捆绑,可以独立运行。