项目需求:
1. 在SpringBoot项目基础上实现登录功能
2.使用传统的Token, 登录成功使用UUID生成对应的Token存到Redis中,并设置过期时间,使用Shiro来完成登录,并对用户实现校验和权限管理
Shiro使用的问题:
Shiro本身使用的传统的Cookie和Session来管理用户信息,因为需要用到Token,所以需要把Shiro本身的Token转化为我们所自定义的Token,
每次登陆以后通过在请求头中增加我们使用的Token来判断是否登录以及是否有权限。
首先引入了Shiro官方提供的SpringBoot包,整合了注解,配置等一些功能,这个包需要在Maven仓库官网搜索才可以导入 (Shiro官网的下载不了-_-) 这里贴下官网https://shiro.apache.org/spring-boot.html
Shiro主要的实现方法是自定义一个自己的Realm通过这里进行身份认证与鉴权
@Slf4j
@Component("myRealm")
public class MyRealm extends AuthorizingRealm {
@Autowired
private ManagerService managerService;
/**
* 提供用户信息返回权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("授权");
Manager manager = (Manager) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole("2");
//在这里可以通过去表里查到用户的角色和权限,加到Shiro中
//TODO:增加角色和权限
return info;
}
/**
* 使用自定义Token替代原生Token
*这里需要自定义一个Token类来表示我们所使用的Token而不是Shiro官方的Token
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof Token;
}
/**
* 提供账户信息返回认证信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//这里的Token就是我们定义的Token,我们需要通过在拦截器去获取我们需要的Token在这里进行登录校验
String myToken = (String) token.getCredentials();
log.info("登录认证" + myToken);
try {
if (myToken == null) {
throw new AuthenticationException("Token didn't existed!");
}
Manager manager = managerService.findByToken(myToken);
if (manager != null) {
//TODO:可以判断是否禁用
//这里返回manager对象是为了在Controller中可以通过
//Manager manager = (Manager)SecurityUtils.getSubject().getPrincipal();来获取我们需要的用户,减少一次查询也不 //需要在请求中传入用户信息
return new SimpleAuthenticationInfo(manager, myToken, getName());
} else {
通过丢出异常让异常处理器接受到从而让接口无法让其他人访问
throw new AuthenticationException("Token didn't existed!");
}
} catch (AuthenticationException e) {
throw new AuthenticationException("Exception!");
}
}
}
接下里是我们自己实现的Token类
public class Token implements AuthenticationToken { //需要继承Shiro的这个类来表示需要代替的Token
/**
* Token
*/
private String token;
public Token(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
接下来就是关键的一步通过在过滤器中找到我们的请求头让我们的Token去Realm中验证
@Slf4j
public class TokenFilter extends BasicHttpAuthenticationFilter {
/**
*登录标识 请求头中带上此标志来进行验证
*/
private static String LOGIN_SIGN = "Token";
/**
* 检测用户是否登录
* 检测header里面是否包含Token字段即可
*
* @param request
* @param response
* @return
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authentication = req.getHeader(LOGIN_SIGN);
log.info(authentication);
if (StringUtil.isEmpty(authentication)) {
log.error("没有token");
}
return authentication != null;
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
log.info("判断请求的请求头是否带上 Token");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//这里让所有的请求除了登录注册都去找Realm验证没有游客功能,过滤登录注册也可以在Config里面配置不过还没有验证
if (httpServletRequest.getRequestURI().equals("/login") || httpServletRequest.getRequestURI().equals("/register")) {
return true;
}
if ((httpServletRequest).getHeader(LOGIN_SIGN) != null) {
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
//token 错误
response401(request, response, e.getMessage());
}
}
//如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
return false;
}
//如果上面的方法返回false也就是没有请求头带token就需要返回到登录界面,因为是前后端分离项目,所以需要通过Json返回给前端,前端再进行判断
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
PrintWriter out = null;
HttpServletResponse res = (HttpServletResponse) response;
res.setStatus(HttpStatus.UNAUTHORIZED.value());
res.setCharacterEncoding("UTF-8");
res.setContentType("application/json; charset=utf-8");
try {
out = res.getWriter();
ResultDTO result = new ResultDTO(false, 401, "请先登录");
out.append(JsonUtil.marshal(result));
} catch (IOException e) {
log.error("返回Response信息出现IOException异常:" + e.getMessage());
} finally {
if (out != null) {
out.close();
}
}
return false;
}
//如果成功来这里进行登录 这里的Token就是我们所设定的Token,将Token获取再去Realm里面进行验证
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
String header = req.getHeader(LOGIN_SIGN);
Token token = new Token(header);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
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);
}
/**
* 401非法请求
*/
private void response401(ServletRequest req, ServletResponse resp,String msg) {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.setStatus(HttpStatus.OK.value());
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
PrintWriter out = null;
try {
out = httpServletResponse.getWriter();
ResultDTO result = new ResultDTO(false, 401, msg);
out.append(JsonUtil.marshal(result));
} catch (IOException e) {
log.error("返回Response信息出现IOException异常:" + e.getMessage());
} finally {
if (out != null) {
out.close();
}
}
}
}
拦截器配置好就可以去配置Shiro的配置
@Configuration //来表示是一个配置类
public class ShiroConfig {
//前几个配置不知道啥意思,好像没啥用(-_-)
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public static DefaultAdvisorAutoProxyCreator getLifecycleBeanPostProcessor() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
@Bean
public DefaultWebSubjectFactory subjectFactory() {
return new SubjectFactory();
}
//将我们写好的的realm注入
@Bean
public Realm realm() {
return new MyRealm();
}
//在这里配置我们的过滤器
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//注意过滤器配置顺序 不能颠倒
Map
filterMap.put("token", tokenFilter()); //这里加入我们的过滤器
shiroFilterFactoryBean.setFilters(filterMap);
filterChainDefinitionMap.put("/**", "token"); //这里的名字需要和上面添加的相同
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
//需要把Shiro的cookie和session关闭但是好像网上找不到正确的方法,每次请求都会带一个sessionId的cookie这以后还要多分析下
@Bean
public DefaultWebSecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
//关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator() {
DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
//我们所自定义的过滤器
@Bean
public TokenFilter tokenFilter() {
return new TokenFilter();
}
}
Shiro默认的没有登录是跳转login.jsp因为是前后端分离的项目所以需要修改为返回Json,但是我没有用到这个先写一下
public class MyAuthenticationFilter extends FormAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return super.isAccessAllowed(request, response, mappedValue);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
Subject subject = SecurityUtils.getSubject();
Object user = subject.getPrincipal();
if (((HttpServletRequest) request).getHeader("Authentication") == null) {
ResultDTO resultDTO = new ResultDTO(false, 401, "未登录");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
httpServletResponse.getWriter().write(JSONObject.toJSONString(resultDTO, SerializerFeature.WriteMapNullValue));
}
return false;
}
}
接下来就是登录了
@PostMapping("/login")
public ResultDTO managerLogin(@RequestBody Map
//这里不要去用Shiro的登录来登录,需要我们自己去数据库查询,所以就不写了
String token = UUID.randomUUID().toString();
//存入Redis并设置过期时间
redisTemplate.opsForValue().set(String.format(RedisConstant.TOKEN_PREFIX, token), username, expire, TimeUnit.SECONDS);
//在响应头添加Token让前端来获取
response.setHeader("Token", token);
}
通过注解来进行角色权限判断
@RequiresGuest
只有游客可以访问
@RequiresAuthentication
需要登录才能访问
@RequiresUser
已登录的用户或“记住我”的用户能访问
@RequiresRoles
已登录的用户需具有指定的角色才能访问
@RequiresPermissions
已登录的用户需具有指定的权限才能访问