上一篇简单了解Shiro框架的几个比较重要的类,清楚在使用Shiro应该配置哪些。本篇则从实际角度出发开发整合登陆认证和权限控制案例。
本实例环境:IDEA + Maven
本实例采用的主要技术:SpringBoot +Shiro+Thymeleaf
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redisartifactId>
<version>2.4.2.1-RELEASEversion>
dependency>
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
public class ShiroConfig {
/**
* 获取资源接口;
*/
@Autowired(required = false)
private ResourcesService resourcesService;
private String host;
private int port;
private int timeout;
/**
* shiro生命周期;
* @return
*/
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* ShiroDialect,为了在thymeleaf里使用shiro的标签的bean
* @return
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在
* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
*
Filter Chain定义说明
1、一个URL可以配置多个Filter,使用逗号分隔
2、当设置多个过滤器时,全部验证通过,才视为通过
3、部分过滤器可指定参数,如perms,roles
*
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
//
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/usersPage");
// 未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
// 拦截器.
Map filterChainDefinitionMap = new LinkedHashMap();
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/css/**","anon");
filterChainDefinitionMap.put("/js/**","anon");
filterChainDefinitionMap.put("/img/**","anon");
filterChainDefinitionMap.put("/font-awesome/**","anon");
//:这是一个坑呢,一不小心代码就不好使了;
//
//自定义加载权限资源关系
List resourcesList = resourcesService.queryAll();
for(Resources resources:resourcesList){
if (StringUtil.isNotEmpty(resources.getResurl())) {
String permission = "perms[" + resources.getResurl()+ "]";
filterChainDefinitionMap.put(resources.getResurl(),permission);
}
}
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置realm.
securityManager.setRealm(myShiroRealm());
// 自定义缓存实现 使用redis
//securityManager.setCacheManager(cacheManager());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
return securityManager;
}
@Bean
public MyShiroRealm myShiroRealm(){
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
/**
* 凭证匹配器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* 所以我们需要修改下doGetAuthenticationInfo中的代码;
* )
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param
* @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
* @return
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setExpire(1800);// 配置缓存过期时间
redisManager.setTimeout(timeout);
// redisManager.setPassword(password);
return redisManager;
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
* @return
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
* shiro session的管理
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
在ShiroConfig
配置类首先从securityManager
方法看,在该方法中需要设置Realm,SessionManager,CacheManager等等,因为SecurityManager是Shiro的主入口,典型的Facade模式,几乎所有相关的权限操作,都由他代理了。
其次在shirFilter
方法中通过ShiroFilterFactoryBean创建shiroFilter,Shiro默认fitler配置有这些:
public enum DefaultFilter {
anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);
···
}
其权限过滤器及配置释义:
权限写法 | 常见用法 | 备注 |
---|---|---|
anon | /admins/**=anon | 没有参数,表示可以匿名使用 |
authc | /admins/user/**=authc | 表示需要认证(登录)才能使用,没有参数 |
roles | /admins/user/**=roles[“admin,guest”] | 每个参数通过才算通过,相当于hasAllRoles()方法。 |
perms | /admins/user/*=perms[“user:add:,user:modify:*”] | 当有多个参数时必须每个参数都通过才通过,想当isPermitedAll()方法 |
rest: | /admins/user/**=rest[user] | 根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。 |
authcBasic | /admins/user/**=authcBasic | 没有参数表示httpBasic认证 |
ssl | /admins/user/**=ssl | 没有参数,表示安全的url请求,协议为https |
user | /admins/user/**=user | 没有参数表示必须存在用户,当登入操作时不做检查 |
port | /admins/user/**=port[8081] | 当请求的url的端口不是8081则跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数 |
在Controller层接口接受VO请求数据, 在认证过程中,用户需要提交实体信息(Principals
)和凭据信息(Credentials
)以检验用户是否合法。UsernamePasswordToken
支持最常见的用户名/密码的认证机制,通过subject.login(token)
提交认证操作。
/**
* POST提交数据;
*
* @param request requst请求;
* @param user user对象数据;
* @param model
* @return
*/
@PostMapping(value="/login")
public String login(HttpServletRequest request, User user, Model model){
if (StringUtils.isEmpty(user.getUsername()) || StringUtils.isEmpty(user.getPassword())) {
request.setAttribute("msg", "用户名或密码不能为空!");
return "login";
}
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token=new UsernamePasswordToken(user.getUsername(),user.getPassword());
try {
subject.login(token);
return "redirect:usersPage";
}catch (LockedAccountException lae) {
token.clear();
request.setAttribute("msg", "用户已经被锁定不能登录,请与管理员联系!");
return "login";
} catch (AuthenticationException e) {
token.clear();
request.setAttribute("msg", "用户或密码不正确!");
return "login";
}
}
目前还没做前后端分离,所以返回时通过页面跳转来实现。认证通过后跳转至usersPage请求对应的页面,错误则跳转至login页面。
第一篇提到过,在Shiro中,最终是通过Realm来获取应用程序中的用户、角色及权限信息的,通过覆写doGetAuthenticationInfo
完成认证操作,
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户的输入的账号.
String username = (String)token.getPrincipal();
User user = userService.selectByUsername(username);
if(user==null){
throw new UnknownAccountException();
}
if (0==user.getEnable()) {
throw new LockedAccountException(); // 帐号锁定
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user, //用户
user.getPassword(), //密码
ByteSource.Util.bytes(username),
getName() //realm name
);
// 当验证都通过后,把用户信息放在session里
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute("userSession", user);
session.setAttribute("userSessionId", user.getId());
return authenticationInfo;
}
该方法主要执行以下操作:
1. 通过token.getPrincipal()
获取令牌信息,该数据对应Controller层UsernamePasswordToken(user.getUsername(),user.getPassword());
中的user.getUsername()
;
2. 根据令牌信息从数据源(通常为数据库)中获取用户对象 ;
3. 针对返回结果进行验证;
4. 验证通过将返回一个封装了用户信息的AuthenticationInfo
实例;
5. 验证失败则抛出AuthenticationException
异常信息。
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:单独一个ShiroFilterFactoryBean配置是报错的,因为在
* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
*
Filter Chain定义说明
1、一个URL可以配置多个Filter,使用逗号分隔
2、当设置多个过滤器时,全部验证通过,才视为通过
3、部分过滤器可指定参数,如perms,roles
*
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
//
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/usersPage");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//拦截器.
Map filterChainDefinitionMap = new LinkedHashMap();
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
//:这是一个坑呢,一不小心代码就不好使了;
//
//自定义加载权限资源关系
List resourcesList = resourcesService.queryAll();
for(Resources resources:resourcesList){
if (StringUtil.isNotEmpty(resources.getResurl())) {
String permission = "perms[" + resources.getResurl()+ "]";
filterChainDefinitionMap.put(resources.getResurl(),permission);
}
}
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
到此关键配置已完成,接下来运行程序进行验证。
项目下载地址:springboot-shiro-2 整合登陆认证和权限控制