Shiro
Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。并且相对于其他安全框架,Shiro 要简单的多。
整合shiro
参考官方文档
考虑到后面可能需要做集群、负载均衡等,所以就需要会话共享,而shiro的缓存和会话信息,我们一般考虑使用redis来存储这些数据,所以,我们不仅仅需要整合shiro,同时也需要整合redis。在开源的项目中,我们找到了一个starter可以快速整合shiro-redis,配置简单,这里也推荐大家使用。
首先要先导入依赖
导入shiro-redis的starter包:还有jwt的工具包,以及为了简化开发,引入了hutool工具包。
<!-- shiro -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis-spring-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!-- huitool工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.3</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
ShiroConfig
@Configuration
public class ShiroConfig {
@Autowired
JwtFilter jwtFilter;
@Bean
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// inject redisSessionDAO
sessionManager.setSessionDAO(redisSessionDAO);
// other stuff...
return sessionManager;
}
@Bean
public DefaultWebSecurityManager securityManager(AccountRealm accountRealm, SessionManager sessionManager, RedisCacheManager redisCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
securityManager.setSessionManager(sessionManager);
securityManager.setCacheManager(redisCacheManager);
/*
* 关闭shiro自带的session
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map<String,String> filterMap = new LinkedHashMap<>();
//主要通过注解方式校验权限
filterMap.put("/**","jwt");
chainDefinition.addPathDefinitions(filterMap);
return chainDefinition;
}
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition){
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", jwtFilter);
shiroFilter.setFilters(filters);
Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
}
- 引入RedisSessionDAO和RedisCacheManager,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享。
- 重写了SessionManager和DefaultWebSecurityManager,同时在DefaultWebSecurityManager中为了关闭shiro自带的session方式,我们需要设置为false,这样用户就不再能通过session方式登录shiro。后面将采用jwt凭证登录。
- 在ShiroFilterChainDefinition中,我们不再通过编码形式拦截Controller访问路径,而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息,有就登录,没有就跳过。跳过之后,有Controller中的shiro注解进行再次拦截,比如@RequiresAuthentication,这样控制权限访问。
新建一个Realm类
新建的realm类需要继承Shiro包下的AuthorizingRealm类 并重写doGetAuthorizationInfo和doGetAuthenticationInfo方法
doGetAuthorizationInfo获取身份信息,我们可以在这个方法中,从数据库获取该用户的权限和角色信息,当调用权限验证时,就会调用此方法
doGetAuthenticationInfo在这个方法中,进行身份验证 , login时调用
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
JwtUtils jwtUtils;
@Autowired
CardService cardService;
@Override
public boolean supports(AuthenticationToken token) {
//realm支持jwt的凭证校验
return token instanceof JwtToken;
}
/**
*获取身份信息,我们可以在这个方法中,从数据库获取该用户的权限和角色信息 当调用权限验证时,就会调用此方法
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 在这个方法中,进行身份验证 login时调用
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
JwtToken jwtToken = (JwtToken) authenticationToken;
//这里演示就随便写的接口 到时候这里写你们登录验证时的业务操作即可
String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();
Card card = cardService.getById(Long.valueOf(userId));
if(card == null){
throw new UnknownAccountException("账户不存在");
}
AccountProfile accountProfile = new AccountProfile();
BeanUtil.copyProperties(card,accountProfile);
return new SimpleAuthenticationInfo(accountProfile,jwtToken.getCredentials(),getName());
}
}
主要就是doGetAuthenticationInfo登录认证这个方法,通过jwt获取到用户信息,判断用户的状态,最后异常就抛出对应的异常信息,否者封装成SimpleAuthenticationInfo返回给shiro
AccountProfile根据自己的业务填写参数(登录成功之后返回的一个用户信息的载体)
@Data
public class AccountProfile implements Serializable {
private Long id;
private Date addTime;
private String phone;
private String name;
}
新建一个JWTFilter
@Component
public class JwtFilter extends AuthenticatingFilter {
@Autowired
JwtUtils jwtUtils;
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//将servletRequest强转为HttpServletRequest
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = httpServletRequest.getHeader("Authorization");
if (jwt.isEmpty()){
return null;
}
return new JwtToken(jwt);
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = httpServletRequest.getHeader("Authorization");
//这里为空将不再进行shiro登录处理 直接交给注解拦截就行了
if (StringUtils.isEmpty(jwt)){
return true;
}else {
//校验jwt
Claims claim = jwtUtils.getClaimByToken(jwt);
if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())){
throw new ExpiredCredentialsException("token已经失效,请重新登录");
}
//执行登录处理
return executeLogin(servletRequest,servletResponse);
}
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
//将异常返回给前端
HttpServletResponse servletResponse = (HttpServletResponse) response;
Throwable throwable = e.getCause() == null ? e : e.getCause();
Result result = new Result(400, throwable.getMessage(), null);
String jsonStr = JSONUtil.toJsonStr(result);
try {
servletResponse.getWriter().print(jsonStr);
} catch (IOException ex) {
}
return false;
}
}
JwtToken
新建一个jwtToken实现AuthenticationToken
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token){
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
JwtUtils
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* jwt工具类
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "jwtutils.jwt")
public class JwtUtils {
private String secret;
private long expire;
private String header;
/**
* 生成jwt token
*/
public String generateToken(long userId) {
Date nowDate = new Date();
//过期时间
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId+"")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getClaimByToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
log.debug("validate is token error ", e);
return null;
}
}
/**
* token是否过期
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
}
全局异常处理
这里我就随便写了两个异常 如果后期有其他异常也可以加上去
@RestControllerAdvice
public class GlobalExceptionHeadler {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = RuntimeException.class)
public Result headler(RuntimeException e){
return new Result(400,e.getMessage(),null);
}
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(value = ShiroException.class)
public Result headler(ShiroException e){
return new Result(401,e.getMessage(),null);
}
}
返回对象
这里只是一个简单的返回对象 可以根据你们业务进行扩展
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
public Result(Integer code,String message){
this(code,message,null);
}
}
配置类
shiro-redis:
enabled: true
redis-manager:
host: 127.0.0.1:6379
jwtutils:
jwt:
#加密密钥
secret: f1231x545as21xc5a4sz2c1sa
#token有效时间 单位秒
expire: 604800
header: Authorization
启动前 在对应的controller加入注解@RequiresAuthentication即可
未接入之前
接入之后 因为没有token所以直接抛异常了