引言
JWT与Shiro结构示意:JWT无需Cookie,而Shiro可在任何环境下使用session(其自定义session会话管理
当同一个realm域分别拆成构造安全数据(放用户私有服务)与获取安全数据(放公共模块)的两个类时,对应的配置文件在构建realm时得return对应的类。
@Bean
public LecRealm getRealm() {
return new UserRealm(); //这个点很重要 要是获取数据源则 return new LecRealm()
}
Subject:主体,代表了当前“用户”,与当前应用交互的任何东西都是Subject,与Subject的所有交互都会委托给SecurityManager。
参考link
认证
1、先执行Shiro自己的Filter链;2、再执行Servlet容器的Filter链(即原始的Filter)。
常用过滤器名:authc(认证之后访问)、anon(直接放行)
org.apache.shiro.web.filter.authc.FormAuthenticationFilter#FormAuthenticationFilter
isAccessAllowed()-> subject.isAuthenticated() 判断当前session中的subject是否已经登陆过,
没有则执行onAccessDenied()
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (this.isLoginRequest(request, response)) { //看是不是配置的loginUrl(默认为"/login.jsp"
if (this.isLoginSubmission(request, response)) { //看是不是Post请求
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return this.executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
return true;
}
} else {
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication. Forwarding to the Authentication url [" + this.getLoginUrl() + "]");
}
this.saveRequestAndRedirectToLogin(request, response);
//最终用session.setAttribute(SAVED_REQUEST_KEY, savedRequest);保存非loginUrl的请求 然后重定向到loginUrl
return false;
}
}
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = this.createToken(request, response); //最终会创建UsernamePasswordToken
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);
} else {
try {
Subject subject = this.getSubject(request, response); //给该Post请求创建一个subject
subject.login(token); //最终到realm中执行doGetAuthenticationInfo方法
return this.onLoginSuccess(token, subject, request, response); //跳转到之前保存的非loginUrl的地址(没有则是successUrl(默认为"/")),return false prevent the chain from continuing:即不让请求达到loginUrl
} catch (AuthenticationException var5) { //该异常被处理了,就抛不出去了
return this.onLoginFailure(token, var5, request, response); //return true即login failed, let request continue back to the login page:
}
}
}
兜兜转转还是来到了UsernamePasswordToken
protected AuthenticationToken createToken(String username, String password,
boolean rememberMe, String host) {
return new UsernamePasswordToken(username, password, rememberMe, host);
}
登录部分
1.subject.login(upToken);
2.DelegatingSubjects中自动委托给securityManager:Subject subject = securityManager.login(this, token);
3.委托给Authenticator执行真正的身份验证:ModularRealmAuthenticator根据realms个数选择是认证方式
4.调用realm域中的认证:AuthenticationInfo info = realm.getAuthenticationInfo(token);
5_开始之前看下缓存:AuthenticationInfo info = getCachedAuthenticationInfo(token);
5.执行自定义认证:protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
public class UserRealm extends LecRealm { //继承关系且getName没有覆盖(以致于放置和获取是同一个Realm域)
@Autowired
private ShiroService shiroService;
@Autowired
SysUserService sysUserService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
//登录信息
String username = upToken.getUsername(); //用户名 也就是昵称 唯一的
String password = String.valueOf(upToken.getPassword());
//查询用户信息
SysUserEntity user = sysUserService.queryByUserName(username);
//账号锁定
if(user.getStatus() == 0){
throw new LockedAccountException("账号已被锁定,请联系管理员");
}
//账号不存在、密码错误
if(user == null || !user.getPassword().equals(new Sha256Hash(password, user.getSalt()).toHex())) {
throw new UnknownAccountException("账号或密码不正确");
}
Long userId = user.getUserId();
//用户权限列表
Set<String> permsSet = shiroService.getUserPermissions(userId);
//用户角色列表
Set<String> rolesSet = shiroService.getUserRoles(userId);
ProfileResult profileResult = new ProfileResult();
BeanUtils.copyProperties(user,profileResult);
profileResult.setPermsSet(permsSet);
profileResult.setRoleSet(rolesSet);
//将身份信息user 凭着信息accessToken 放入getName()对应的realm
// SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
//由于现在认证\授权要分离了,所以权限信息得先放到realm域
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(profileResult, password, this.getName()); //password要为upToken里的
return info;
}
}
会话管理
6.认证执行完成数据源准备完毕,开始创建Subject loggedIn = createSubject(token, info, subject);
7.将authenticated=true
8.save(subject);->委托给DefaultSubjectDAO:saveToSession(subject);
Session session = subject.getSession(false); //false则不创建session
if (session == null) {
if (!isEmpty(currentPrincipals)) {
session = subject.getSession(); //内部实现getSession(true);
//将subject的Principals存储再subject的session中
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
}
// otherwise no session and no principals - nothing to save
} else {
getSession(true)触发session的创建
9.1 DefaultSessionManager中sessionDAO.create(session); //项目这里的sessionDAO实际类型为RedisSessionDAO
9.2 RedisSessionDAO中进行真正的doCreate:分配sessionId、存储
9.3 依据设置看是否将sessionID放Cookie
9.4 只要session里的内容变了,就会触发update更新保存
//saveSession的关键代码...
key = keySerializer.serialize(getRedisSessionKey(session.getId()));
value = valueSerializer.serialize(session);
...
this.redisManager.set(key, value, expire);
@Override
public void update(Session session) throws UnknownSessionException {
this.saveSession(session);
}
补充:
参考link
当Subject.logout()时会自动调用stop方法来销毁会话
在Servlet容器中,默认使用JSESSIONID Cookie维护会话,且会话默认是跟容器绑定的;在某些情况下(手机等移动客户端不支持Cookie)可能需要使用自己的会话机制,此时我们可以使用DefaultWebSessionManager来维护会话
授权
对于注解@RequiresPermissions(“product:leccategory:list”),用的是动态代理CglibAopProxy
0.对任意的非放行的请求都得执行最上面的认证流程看是否isAccessAllowed()
1.PermissionAnnotationMethodInterceptor
2.开始委派,委派流程和认证特别像,最终由Authorizer真正执行鉴权。
3.AuthrizingRealm中调用自定义的Realm域授权方法,获取用户拥有的所有权限,看是否包含
/**
* 公共的realm
* @author: hyl
* @date: 2020/02/08
**/
public class LecRealm extends AuthorizingRealm {
public void setName(String name){
super.setName("LecRealm");
}
//授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取安全数据
ProfileResult result = (ProfileResult) principalCollection.getPrimaryPrincipal();
//获取当前用户的所有角色信息
Set<String> rolesPerms = result.getRoleSet();
//获取权限信息
Set<String> permsSet = result.getPermsSet();
//授予权限信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(rolesPerms);
info.setStringPermissions(permsSet);
return info;
}
//认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
PS:SpringBoot集成shiro-redis遇到的问题(java.lang.ClassCastException),谁知和dev-tools有冲突:link
项目方案
对于登录的请求,配置类中直接放行:filterMap.put("/sys/login", “anon”);
省得绕来绕去,咱再自己写异常可以统一处理,然后让前端根据返回的code:21000 进行跳转到登录页
/**
* 用户登录
*/
@RequestMapping(value = "/sys/login" , method = RequestMethod.POST)
public R login(@RequestBody SysLoginForm form)throws IOException {
boolean captcha = sysCaptchaService.validate(form.getUuid(), form.getCaptcha());
if(!captcha){
return R.error("验证码不正确");
}
UsernamePasswordToken upToken = new UsernamePasswordToken(form.getUsername() , form.getPassword());
//获取subject
Subject subject = SecurityUtils.getSubject();
//调用login方法,进入realm完成认证
subject.login(upToken);
//获取sessionId
String sessionId = (String) subject.getSession().getId();
return R.ok().put("sessionId",sessionId);
}
shiro配置
/**
* Shiro配置
*
* @author Mark sunlightcs@gmail.com
*/
@Configuration
public class ShiroConfig {
//1.创建realm
@Bean
public LecRealm getRealm() {
return new UserRealm(); //这个点很重要
}
@Bean("securityManager")
public SecurityManager securityManager(LecRealm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
// securityManager.setRememberMeManager(null);
//将自定义的会话管理器注册到安全管理器中
securityManager.setSessionManager(sessionManager());
//将自定义的redis缓存管理器注册到安全管理器中
securityManager.setCacheManager(cacheManager());
return securityManager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
// shiroFilter.setLoginUrl("http://localhost:6060/authError?code=1");//未登录跳转的url
// shiroFilter.setUnauthorizedUrl("http://localhost:6060/authError?code=2");//未授权跳转的url
Map<String, Filter> filters = new HashMap<>();
//oauth过滤
// filters.put("oauth2", new OAuth2Filter());
shiroFilter.setFilters(filters);
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/sys/register", "anon");
filterMap.put("/sys/role/listAll", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon");
filterMap.put("/app/**", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/aaa.txt", "anon");
// filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
//开启对shiro注解的支持
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
//-----------------------------------------------
@Value("${spring.redis.host}")
private String host = "localhost";
@Value("${spring.redis.port}")
private int port=6379;
/**
* 1.redis的控制器,操作redis
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
return redisManager;
}
/**
* 2.sessionDao
*/
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO sessionDAO = new RedisSessionDAO();
sessionDAO.setRedisManager(redisManager());
return sessionDAO;
}
/**
* 3.会话管理器
*/
public DefaultWebSessionManager sessionManager() {
CustomSessionManager sessionManager = new CustomSessionManager();
// sessionManager.setSessionIdCookieEnabled(false); //不使能cookie
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* 4.缓存管理器
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
}