Shiro认证集成
1、介绍
Shiro是开源的JAVA安全框架,实现用户身份认证、权限授权、加密、会话管理等功能。与Spring Security相比,更加轻量级。
2、pom依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
3、核心配置类
@Configuration
public class ShiroConfig {
@Resource
private ShiroRealm shiroRealm;
@Autowired
private ShiroAnonConfig shiroAnonConfig;
@Bean
public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager") SecurityManager securityManager, RedisTemplate<String,String> redisTemplate) {
//创建拦截链实例
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置拦截链map
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 放过自定义白名单连接
shiroAnonConfig.getAnonUri().forEach((uri) -> {
filterChainDefinitionMap.put(uri, "anon");
});
filterChainDefinitionMap.put("/**", "token");
//设置拦截规则给shiro的拦截链工厂
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
// 添加自己的自定义拦截器
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
filterMap.put("token", new ShiroTokenFilter(redisTemplate));
shiroFilterFactoryBean.setFilters(filterMap);
//配置拦截链到过滤器工厂
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//返回实例
return shiroFilterFactoryBean;
}
@Bean
public DefaultWebSecurityManager securityManager() {
//创建默认的web安全管理器
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
//配置shiro的自定义认证逻辑
defaultSecurityManager.setRealm(shiroRealm);
//关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultSecurityManager.setSubjectDAO(subjectDAO);
//返回安全管理器实例
return defaultSecurityManager;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor
= new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
4、核心Relm
自定义token
public class ShiroToken implements AuthenticationToken {
private String token;
public ShiroToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
Relm
@Slf4j
@Component
public class ShiroRealm extends AuthorizingRealm {
@Resource
private UserDetailsService userDetailsService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 设置对应的token类型
* 必须重写此方法,不然Shiro会报错
*
* @param token 令牌
* @return boolean
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof ShiroToken;
}
/**
* 授权认证
*
* @param principals 主要收集
* @return {@link AuthorizationInfo}
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User userAuthInfoDTO = (User) principals.getPrimaryPrincipal();
log.info("---->用户token信息缓存数据:{}", userAuthInfoDTO);
if (null == userAuthInfoDTO) {
return null;
}
List<String> authoritiesList = userDetailsService.getAuthoritiesList(userAuthInfoDTO.getId());
if (CollectionUtils.isEmpty(authoritiesList)) {
return null;
} else {
//设置权限
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(new HashSet<>(authoritiesList));
//返回权限实例
return info;
}
}
/**
* 认证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
String token = (String) authenticationToken.getCredentials();
String key = "LOGIN-" + token;
if (!redisTemplate.hasKey(key)) {
return null;
}
String userJson = redisTemplate.opsForValue().get(key);
User user = JSON.parseObject(userJson, User.class);
if (Objects.nonNull(user)) {
return new SimpleAuthenticationInfo(user, token, "shiroRealm");
}
return null;
}
}
5、核心filter
@Slf4j
public class ShiroTokenFilter extends BasicHttpAuthenticationFilter implements Filter {
private RedisTemplate<String, String> redisTemplate;
public ShiroTokenFilter(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 执行登录
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("token");
log.info("当前请求token:{}", token);
if (token == null) {
return false;
}
ShiroToken shiroToken = new ShiroToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
try {
getSubject(request, response).login(shiroToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
} catch (Exception e) {
log.info("token鉴权异常", e);
return false;
}
}
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
return executeLogin(request, response);
} catch (IOException e) {
return false;
}
}
/**
* 认证失败时,自定义返回json数据
*
* @param request 请求
* @param response 响应
* @return boolean* @throws Exception 异常
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
ResponseResult responseResult = new ResponseResult(500, "鉴权失败");
Object parse = JSONObject.toJSON(responseResult);
response.setCharacterEncoding("utf-8");
response.getWriter().print(parse);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
boolean bool = super.onAccessDenied(request, httpServletResponse);
httpServletResponse.setContentType("application/json");
//设置后,浏览器不会弹出用户验证
httpServletResponse.setHeader("WWW-Authenticate", "");
return bool;
}
/**
* 对跨域提供支持
*
* @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,PATCH,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);
}
}
6、验证
登录登出
@Service
@Slf4j
public class LoginServiceImpl implements LoginServcie {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public ResponseResult login(User user) {
try {
String userName = user.getUserName();
String password = user.getPassword();
User userByName = userDetailsService.getUserByName(userName);
if (Objects.isNull(userByName)) {
return new ResponseResult(500, "用户不存在");
}
if (userByName.getPassword().equals(password)) {
String token = UUID.randomUUID().toString().replaceAll("-", "");
redisTemplate.opsForValue().set("LOGIN-" + token, JSON.toJSONString(userByName), 1, TimeUnit.HOURS);
Map<String, String> result = new HashMap<>();
result.put("token", token);
return new ResponseResult(200, "登录成功", result);
}
} catch (Exception e) {
log.error("login error", e);
}
return new ResponseResult(500, "登陆失败");
}
@Override
public ResponseResult logout() {
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isAuthenticated()) {
currentUser.logout();
}
return new ResponseResult(200, "退出成功");
}
}
权限控制-shiro注解
@RestController
public class HelloController {
@GetMapping("/hello")
@RequiresPermissions("userPerm")
public String hello() {
return "hello";
}
@GetMapping("/hello2")
@RequiresPermissions("dataPerm")
public String hello2() {
Subject subject = SecurityUtils.getSubject();
User principal = (User) subject.getPrincipal();
return "hello2";
}
}
7、总结
1、输入用户名密码登录,此登录跟shiro无关,校验用户名密码,成功之后生成token,redis根据此token保存用户信息.
2、接口调用,shiro认证,进入到ShiroTokenFilter,拼接ShiroToken,调用ShiroRealm->doGetAuthenticationInfo,redis中存在此token信息,拼装SimpleAuthenticationInfo,认证成功。
3、shiro授权,在认证成功后,进入到ShiroRealm->doGetAuthorizationInfo,拼装SimpleAuthorizationInfo,生成权限信息,再跟RequiresPermissions需要的权限进行匹配