Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。
具体详细介绍百度搜索,这里就不再过多描述了。
pom.xml文件
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>auth-service</artifactId>
<version>2.0</version>
<name>auth-service</name>
<description>Shiro auth project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
<scope>compile</scope>
</dependency>
<!--JPA-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--JDBC-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--mysql-connector-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
<!-- shiro+redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目结构图:
实现代码
ShiroConfig配置自定义拦截器AuthFilter,需要过滤的接口;安全管理器SecurityManager;自定义AuthorizingRealm安全登陆和权限认证;自定义加密验证规则和Redis会话缓存。
@Configuration
public class ShiroConfig {
private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
System.out.println("拦截器 =>>>> ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//自定义AuthFilter拦截器 auth过滤
Map<String, Filter> filters = new HashMap<>();
filters.put("auth", new AuthFilter());
shiroFilterFactoryBean.setFilters(filters);
//拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//登陆过滤接口
filterChainDefinitionMap.put("/toLogin", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/auth", "anon");
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/**", "auth");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 安全管理器
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义realm
securityManager.setRealm(myShiroRealm());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(redisCacheManager());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 凭证匹配器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
//散列算法:这里使用MD5算法; 加密两次
CustomCredentialsMatcher matcher = new CustomCredentialsMatcher(SysUserUtils.hashAlgorithmName);
matcher.setHashIterations(SysUserUtils.hashIterations);
matcher.setStoredCredentialsHexEncoded(true);
// HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// hashedCredentialsMatcher.setHashAlgorithmName("md5");
// hashedCredentialsMatcher.setHashIterations(2);
return matcher;
}
@Bean
public AuthRealm myShiroRealm() {
AuthRealm myShiroRealm = new AuthRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Value("${spring.redis.database}")
private Integer database;
@Value("${spring.redis.password}")
private String password;
/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
* @return
*/
public RedisManager getRedisManager() {
RedisManager redisManager = new RedisManager();
// 默认 127.0.0.1:6379
redisManager.setDatabase(database);
redisManager.setPassword(password);
return redisManager;
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
* @return
*/
public RedisCacheManager redisCacheManager() {
logger.info("创建RedisCacheManager...");
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(getRedisManager());
redisCacheManager.setPrincipalIdFieldName("userId");//不添加此编注会异常
return redisCacheManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(getRedisManager());
return redisSessionDAO;
}
/**
* Session Manager 会话管理器
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
}
安全登陆验证、token验证
import com.example.config.token.AuthToken;
import com.example.config.token.SysUserPrincipal;
import com.example.sys.service.SysTokenService;
import com.example.sys.service.SysUserService;
import com.example.sys.entity.SysMenu;
import com.example.sys.entity.SysRole;
import com.example.sys.entity.SysToken;
import com.example.sys.entity.SysUser;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author: tcq
* @date: 2021-11-01 13:19
*/
@Component
public class AuthRealm extends AuthorizingRealm {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysTokenService sysTokenService;
/**
* 授权 获取用户的角色和权限
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//1. 从 PrincipalCollection 中来获取登录用户的信息
SysUserPrincipal userPrincipal = (SysUserPrincipal) principals.getPrimaryPrincipal();
SysUser sysUser = sysUserService.findByUserId(userPrincipal.getUserId());
//2.添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (SysRole role : sysUser.getRoleList()) {
//2.1添加角色
System.out.println(role.getRole());
simpleAuthorizationInfo.addRole(role.getRole());
for (SysMenu permission : role.getPermissions()) {
//2.1.1添加权限
simpleAuthorizationInfo.addStringPermission(permission.getMenu());
System.out.println(permission.getMenu());
}
}
return simpleAuthorizationInfo;
}
/**
* 登录时-shiro自动认证,接口访问验证token
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("AuthRealm.doGetAuthenticationInfo");
//获取token
AuthToken authToken = (AuthToken) authenticationToken;
String token = authToken.getToken();
if (StringUtils.isBlank(token)){ // 登录验证
SysUser sysUser = sysUserService.findByUsername(authToken.getUsername());
if (sysUser == null) {
throw new UnknownAccountException("用户不存在!");
}
SysUserPrincipal userPrincipal = new SysUserPrincipal(sysUser.getUserId(), sysUser.getUsername(), sysUser.getName());
//交给AuthenticationInfo去验证密码是否正确,成功后记录SysUserPrincipal
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
userPrincipal,
sysUser.getPassword(),
ByteSource.Util.bytes(sysUser.getCredentialsSalt()),
getName());
return info;
}else{ //token 认证
//1. 根据accessToken,查询用户信息
SysToken tokenEntity = sysTokenService.findByToken(token);
//2. token失效
if (tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()) {
throw new IncorrectCredentialsException("token失效,请重新登录");
}
//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
SysUser sysUser = sysUserService.findByUserId(tokenEntity.getUserId());
//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
if (sysUser == null) {
throw new UnknownAccountException("用户不存在!");
}
SysUserPrincipal userPrincipal = new SysUserPrincipal(sysUser.getUserId(), sysUser.getUsername(), sysUser.getName());
//5. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userPrincipal, token, this.getName());
return info;
}
}
}
自定义凭据匹配器
package com.example.config.shiro;
import com.example.config.token.AuthToken;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
/**
* 自定义凭据匹配器
* @author: tcq
* @date: 2021-11-02 10:09
*/
public class CustomCredentialsMatcher extends HashedCredentialsMatcher {
public CustomCredentialsMatcher(String hashAlgorithmName) {
super(hashAlgorithmName);
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
AuthToken authToken = (AuthToken) token;
if (StringUtils.isNotBlank(authToken.getToken())){
return true;
}
//交给shiro验证密码是否正确
return super.doCredentialsMatch(token, info);
}
}
自定义拦截器AuthFilter
package com.example.config.shiro;
import com.example.config.token.AuthToken;
import com.example.sys.utils.TokenUtil;
import com.example.sys.utils.R;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* shiro 自定义拦截器
* @author: tcq
* @date: 2021-11-01 11:47
*/
@Component
public class AuthFilter extends AuthenticatingFilter {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final Logger logger = LoggerFactory.getLogger(AuthFilter.class);
@Override
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//String referer = httpServletRequest.getHeader("Referer");
//System.out.println("referer = " + referer);
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
logger.info("doFilterInternal请求地址:"+ (httpServletRequest).getRequestURI());
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("text/plain;charset=utf-8");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "Content-type, accept, token");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
/*if (StringUtils.isNotBlank(referer)){
if (referer.endsWith("/")){
referer = referer.substring(0, referer.length() - 1);
}
//httpServletResponse.setHeader("Access-Control-Allow-Origin", referer);
}*/
super.doFilterInternal(request, response, chain);
}
/**
* 3、创建Token对象
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
System.out.println("AuthFilter.createToken");
String token = TokenUtil.getRequestToken((HttpServletRequest) request);
return new AuthToken(token);
}
/**
* 步骤1.所有请求全部拒绝访问
* @param servletRequest
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse response, Object mappedValue) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
if (request.getMethod().equals(RequestMethod.OPTIONS.name())){
return true;
}
return false;
}
/**
* 步骤2,拒绝访问的请求,会调用onAccessDenied方法,onAccessDenied方法先获取请求中的token,再调用executeLogin方法
* executeLogin方法执行 this.createToken 再执行 getSubject(request, response).subject.login(token);
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
System.out.println("AuthFilter.onAccessDenied");
//获取请求token,如果token不存在,直接返回
String token = TokenUtil.getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
responseCode(response, "请先登录");
//httpResponse.sendRedirect(((HttpServletRequest) request).getContextPath() + "/toLogin");
return false;
}
return executeLogin(request, response);
}
/**
* token失效时候调用
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
System.out.println("AuthFilter.onLoginFailure");
//处理登录失败的异常
responseCode(response, "登录凭证已失效,请重新登录");
return false;
}
/**
* 缺少token或token失效时返回
* @param servletResponse
* @param msg
* @return
*/
private boolean responseCode(ServletResponse servletResponse, String msg) {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
try {
String json = MAPPER.writeValueAsString(R.error(401, msg));
response.getWriter().print(json);
response.getWriter().flush();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
}
验证流程说明
登陆认证
以上4个核心配置文件,在登陆接口执行过程中,subject.login(authToken);使用了内部验证,调用doGetAuthenticationInfo()方法,因为是登陆请求,不带token,通过username获取用户信息,把用户信息交给shiro内部认证,验证密码时在CustomCredentialsMatcher中调用内部验证方法super.doCredentialsMatch(token, info);
网上有很多案例,是通过密码明文加密比对数据库密文是否相同,来验证是否登陆成功,此处采用了shiro原生的验证方式。
接口token认证
对于非过滤的接口,需要token认证访问。
在自定义拦截器AuthFilter中
步骤一:isAccessAllowed()拦截请求
步骤二:onAccessDenied()检查token参数,必须携带才能继续访问,内部执行源码
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
"must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
步骤三:创建AuthenticationToken对象去验证
步骤四:AuthRealm.doGetAuthenticationInfo()验证token有效性,验证绑定的用户有效性
步骤五:上一步如验证成功,CustomCredentialsMatcher.doCredentialsMatch()返回true通过认证;如验证失败onLoginFailure()返回json失败消息。
权限认证
注解标注
@RequiresPermissions("user:list")//权限标识
管理员or测试员访问
@RequiresRoles(value = {"admin", "test"}, logical = Logical.OR)
认证方法,AuthRealm.doGetAuthorizationInfo(),验证后会缓存起来;验证失败异常监听返回json消息
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler({UnauthorizedException.class, AuthorizationException.class, Exception.class})
@ResponseBody
public R authorizationException(Exception ex) {
R r = R.error();
if (ex instanceof AuthorizationException || ex instanceof UnauthorizedException) {
r = R.error(403, "没有操作权限");
}
return r;
}
}
登陆接口
http://localhost:6080/shiro/auth
登陆成功后,生成token并返回
@RequestMapping("/auth")
@ResponseBody
public R testLogin(@RequestParam String username, @RequestParam String password) {
Subject subject = SecurityUtils.getSubject();
//测试
AuthToken authToken = new AuthToken(username, password);
try {
subject.login(authToken);
}catch (UnknownAccountException e){
return R.error("账号不存在");
}catch (IncorrectCredentialsException e){
return R.error("密码不正确");
}catch (Exception e){
e.printStackTrace();
return R.error(e.getMessage());
}
SysUserPrincipal user = getUser();
return R.ok(user).put("token", sysTokenService.createToken(user.getUserId()));
}
token生成规则
public String createToken(Long userId) {
//生成一个token
String token = RandomStringUtils.randomAlphanumeric(16);
//当前时间
Date now = new Date();
//过期时间
Date expireTime = new Date(now.getTime() + EXPIRE * 1000);
//判断是否生成过token
SysToken tokenEntity = tokenRepository.findByUserId(userId);
if (tokenEntity == null) {
tokenEntity = new SysToken();
tokenEntity.setUserId(userId);
tokenEntity.setToken(token);
tokenEntity.setUpdateTime(now);
tokenEntity.setExpireTime(expireTime);
//保存token
tokenRepository.save(tokenEntity);
} else {
//token 没过期 继续使用
if (tokenEntity.getExpireTime().getTime() > System.currentTimeMillis()) {
token = tokenEntity.getToken();
}
tokenEntity.setToken(token);
tokenEntity.setUpdateTime(now);
tokenEntity.setExpireTime(expireTime);
//更新token
tokenRepository.save(tokenEntity);
}
//redisUtils.set(authKey + token, tokenEntity, EXPIRE);
return token;
}
项目源码:https://download.csdn.net/download/qq_36100599/38488131