现在都是token无状态的认证,今天记录一下如何使用shiro完成token登录认证,话不多说,直接上代码
依赖
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
shiro的主配置类
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
bean.setLoginUrl("/login");
// 添加自定义过滤器
Map<String, Filter> filterMap = new HashMap<>(16);
filterMap.put("tokenFilter", new TokenFilter());
bean.setFilters(filterMap);
/**
* 自定义拦截规则
*/
Map<String, String> filterRuleMap = new LinkedHashMap<>();
filterRuleMap.put("/login","anon");
filterRuleMap.put("/noPer","anon");//不需要认证,可以直接访问的
// 其余请求都要经过BearerTokenFilter自定义拦截器
filterRuleMap.put("/**", "tokenFilter");
bean.setFilterChainDefinitionMap(filterRuleMap);
return bean;
}
@Bean(name="securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
/**
* 托管userrealm
*/
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
userRealm.setAuthenticationTokenClass(JwtToken.class);//如果不进行设置就默认UsernamePasswordToken
securityManager.setRealm(userRealm);
/**
* 禁用session
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 自定义realm
* @return
*/
@Bean(name = "userRealm")
public UserRealm userRealm(){
return new UserRealm();
}
//开启shiro aop注解支持 作用在方法上
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 让注解权限生效(如果注解权限不生效)
* @return
*/
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
return new DefaultAdvisorAutoProxyCreator();
}
}
自定义realm实现类
import java.util.Arrays;
import java.util.List;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import com.spring.framework.entity.LoginUser;
import com.spring.framework.info.DataContextSupport;
import com.spring.framework.utils.RedisUtil;
public class UserRealm extends AuthorizingRealm{
@Autowired
RedisUtil redisUtil;
/**
* 等同于配置类的指定token类型
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
LoginUser user = (LoginUser)principals.getPrimaryPrincipal();//获取到用户
String[] perms = {"sys:user:del","sys:user:add","sys:user:update"};
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if(user.getUserName().equals("ljw")){//该用户拥有所有权限
info.addStringPermission("*:*:*");
info.addRole("*");
}else {
List<String> asList = Arrays.asList(perms);
asList.forEach(str->{
if(!StringUtils.isEmpty(str)){
info.addStringPermission(str);
}
});
}
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
JwtToken jwtToken = (JwtToken)authenticationToken;
String token = (String) jwtToken.getPrincipal();
//token是否存在
if (StringUtils.isEmpty(token)) {
throw new AuthenticationException(" 非法请求,请先登录! ");
}
LoginUser loginUser = (LoginUser)redisUtil.get(token);
if(loginUser==null) {
throw new AuthenticationException(" 登录过期,请重新登录 ");
}
long currentTimeMillis = System.currentTimeMillis();
long e= 30 * 60 * 1000L;//三十分钟
if(currentTimeMillis>=loginUser.getExpireTime()) {
throw new AuthenticationException(" 登录过期,请重新登录 ");
}
long refresTime= loginUser.getExpireTime()-System.currentTimeMillis();
if(refresTime<=5 * 60 * 1000L) {//有效期小于等于五分钟,刷新token
long expireTime = currentTimeMillis+e;
loginUser.setExpireTime(expireTime);
}
redisUtil.set(token, loginUser, 60*30);//redis token再次刷新值
DataContextSupport.setDataPermissions(loginUser);
return new SimpleAuthenticationInfo(loginUser,jwtToken.getPrincipal(),getName());
}
}
自定义token
import org.apache.shiro.authc.AuthenticationToken;
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
token过滤器
import java.io.IOException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import mlt.boot.blog.result.Result;
public class TokenFilter extends BasicHttpAuthenticationFilter{
//源码注释
// Check whether the current request's method requires authentication.
// If no methods have been configured, then all of them require auth,
// otherwise only the declared ones need authentication.
/**
* 作用:个人感觉判断是否需要认证,需要认证的话就进入下面的逻辑
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return super.isAccessAllowed(request, response, mappedValue) ||(!isLoginRequest(request, response) && isPermissive(mappedValue));
}
/**
* 认证未通过执行该方法
* @param request
* @param response
* @return
* @throws IOException
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException{
//完成token登入
//1.检查请求头中是否含有token
HttpServletRequest httpServletRequest= (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
String token = httpServletRequest.getHeader("Authorization");
JwtToken jwtToken = new JwtToken(token);
try {
SecurityUtils.getSubject().login(jwtToken);
/**
* 这里抛异常处理
* Result(code=400, msg=Realm [mlt.boot.blog.config.UserRealm@2e2bff56] does not support authentication
* token [mlt.boot.blog.config.JwtToken@1db51365].
* Please ensure that the appropriate Realm implementation is configured correctly or that the realm accepts
* AuthenticationTokens of this type., data=null)
*/
} catch (AuthenticationException e) {
//这里在全局异常里面捕获不到,所以把信息封装成json返回
Throwable throwable = e.getCause() == null ? e : e.getCause();
Result r = Result.fail(throwable.getMessage());
String json = JSONObject.toJSONString(r);
httpServletResponse.setHeader("content-type", "text/html;charset=UTF-8");//防止乱码
httpServletResponse.getWriter().print(json);
return false;
}
return true;
}
/**
* 方法执行前
* 对跨域提供支持
* @param request
* @param response
* @return
* @throws Exception
*/
@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);
}
}
我的几个请求测试
@PostMapping("/login")
public Result login(@RequestBody LoginUser loginUser) {
String usrename= loginUser.getUserName();
String password= loginUser.getPassWord();
if(StringUtils.isEmpty(usrename)||StringUtils.isEmpty(password)) {
Result.fail("登录参数不能为空");
}
if(usrename.equals("ljw")&&password.equals("ljw")) {
String uuid = UUID.randomUUID().toString().replace("-", "");
loginUser.setUuid(uuid);
long currentTimeMillis = System.currentTimeMillis();
long e= 30 * 60 * 1000L;//三十分钟
long expireTime = currentTimeMillis+e;
loginUser.setExpireTime(expireTime);
redisUtil.set(uuid, loginUser, 60*30);//redis三十分钟有效期
return Result.ok(uuid,"登录成功");
}else if(usrename.equals("test")&&password.equals("test")){
String uuid = UUID.randomUUID().toString().replace("-", "");
loginUser.setUuid(uuid);
long currentTimeMillis = System.currentTimeMillis();
long e= 30 * 60 * 1000L;//三十分钟
long expireTime = currentTimeMillis+e;
loginUser.setExpireTime(expireTime);
redisUtil.set(uuid, loginUser, 60*30);//redis三十分钟有效期
return Result.ok(uuid,"登录成功");
}else {
return Result.fail("用户可能不存在");
}
}
/**
* 获取当前登录用户信息
* @return
*/
@GetMapping("/userInfo")
public Result getUserInfo() {
LoginUser loginUser = DataContextSupport.getDataPermissions();
return Result.ok(loginUser);
}
@GetMapping("/test1")
@RequiresPermissions("sys:user:del")
public Result test1() {
LoginUser loginUser = DataContextSupport.getDataPermissions();
return Result.ok(loginUser);
}
@GetMapping("/test2")
@RequiresPermissions("sys:role:delete")
public Result test2() {
LoginUser loginUser = DataContextSupport.getDataPermissions();
return Result.ok(loginUser);
}
登录实体类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户登录
* @author ljw
*
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements Serializable{
private static final long serialVersionUID = 1L;
private String userName;
private String passWord;
private String uuid;
private Long expireTime;//过期时间
//private SysUser sysUser;//可存放数据库用户信息
}
DataContextSupport
import org.springframework.stereotype.Component;
import com.spring.framework.entity.LoginUser;
import java.util.Optional;
/**
* @author ljw
* 2021年6月19日17:04:083
*/
@Component
public class DataContextSupport {
public static final ThreadLocal<LoginUser> dataContext = new ThreadLocal<>();
private static final LoginUser DATA_PERMISSIONS = new LoginUser();
/**
* 设置当前用户信息
*/
public static void setDataPermissions(LoginUser loginUser) {
dataContext.set(loginUser);
}
/**
* 获取当前用数据信息
*/
public static LoginUser getDataPermissions() {
return Optional.ofNullable(dataContext.get()).orElse(DATA_PERMISSIONS);
}
/**
* 移除当前用户数据信息
*/
public static void close() {
dataContext.remove();
}
}
拦截器,处理DataContextInterceptor的,防止内存溢出,最后记得配置。让拦截器生效
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
/**
* 释放资源
* @author ljw
*
*/
public class DataContextInterceptor implements HandlerInterceptor {
/**
* 拦截前
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 拦截后
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
DataContextSupport.close();
}
}
登录测试
没有登录去请求
访问有权限的
访问无权限的
最后贴上全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
private final static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler. class);
/**
* 无权限操作异常
* @param req
* @return
*/
@ExceptionHandler(AuthorizationException.class)
public Result NoPerm(HttpServletRequest req){
logger.info("请求[ "+req.getRequestURI()+" ]验证未通过:{}", "无权限操作");
return Result.fail("无权限操作",403);
}
@ExceptionHandler(value= RuntimeException.class)
public Result runTimeException(RuntimeException e, HttpServletRequest req){
logger.error("错误请求地址URI [{}]", req.getRequestURI());
return Result.fail(e.getMessage());
}
}