一、引入相关依赖
后面两个依赖可以不引入,还没有使用过Redis来做Shiro的缓存。后续如果有用到,可能会更新到博文。
<!-- Shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- Shiro-Thymeleaf -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Redis缓存Shiro - 本文未使用,可不引入 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
二、基础准备
1、创建用户类(User)
@Data
public class User {
/** 主键Id */
private Long id;
/** 账号 */
private String username;
/** 密码 */
private String password;
private List<Role> roles;
}
2、创建Service层(UserService)
@Service
public class UserServiceImpl implements UserService {
@Override
public User getUserByUsername(String username) {
User user = new User();
// 应该从数据库获取,这里写死了
user.setId(1L);
user.setUsername("lcy123456");
user.setPassword("97fe5ea8b72e6a39bd9e500cb462e426");
return user;
}
@Override
public List<String> getRolesById(Long id) {
List<String> roles = new ArrayList<>();
// 应该从数据库获取,这里写死了
roles.add("hr");
roles.add("manager");
return roles;
}
@Override
public List<String> getPermissionById(Long id) {
List<String> permissions = new ArrayList<>();
// 应该从数据库获取,这里写死了
permissions.add("role:index");
permissions.add("menu:index");
return permissions;
}
}
3、静态登录页准备
添加按钮在这里的意义是本来想演示:通过这个按钮发起请求,但是没有这个权限的解决办法。(实际没啥用,因为我这里没有权限是通过@ControllerAdvice
做了统一处理),只需关注登录即可。
三、自定义AuthorizingRealm
创建一个类继承自AuthorizingRealm
,这个类的作用就是用来认证与授权的。
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 身份认证
* 前端form表单通过post请求发送的/login请求
* 会自动将name为username和password的值放到token里去
* 当然也可以自己手动提交到token里去 - 本例的做法
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 从Token中获取账户
String username = (String) authenticationToken.getPrincipal();
// String password = new String((char[]) authenticationToken.getCredentials()); // 获取密码
// 直接根据获取到的username去数据库登录用户对象
User user = userService.getUserByUsername("lcy123456");
if(user == null){
throw new AccountException("用户名或密码错误!");
}
// 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配
// 参数:主体、正确的密码、盐、当前realm名称
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
user,
user.getPassword(),
ByteSource.Util.bytes(user.getUsername()),
this.getName()
);
return info;
}
/**
* 用户授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取用户身份信息
User user = (User) principalCollection.getPrimaryPrincipal();
// 根据当前角色去查询权限
List<String> roles = userService.getRolesById(user.getId());
List<String> permissions = userService.getPermissionById(user.getId());
// 给授权信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roles);
info.addStringPermissions(permissions);
return info;
}
}
四、Shiro配置文件
@Configuration
public class ShiroConfig {
/**
* 配置ShiroDialect,用于Shiro和thymeleaf标签配合使用
* 可以让Thymealf页面使用shiro标签
* @return
*/
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
/**
* 将自定义Realm交给Spring管理
* @return UserRealm
*/
@Bean
public UserRealm userRealm(){
UserRealm userRealm = new UserRealm();
// 告诉Realm,使用credentialsMatcher加密算法类来验证密文
userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
// 设置不允许缓存
userRealm.setCachingEnabled(false);
// userRealm.setAuthenticationCachingEnabled(true); // 允许认证缓存
// userRealm.setAuthenticationCacheName("authenticationCache");
// userRealm.setAuthorizationCachingEnabled(true); // 允许授权缓存
// userRealm.setAuthorizationCacheName("authorizationCache");
return userRealm;
}
/**
* Shiro核心类:协调Shiro内部的各种安全组件
* @return SecurityManager
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm
securityManager.setRealm(userRealm());
// // 自定义缓存实现 - 使用Redis
// securityManager.setCacheManager(cacheManager());
// // 自定义session管理 - 使用redis
// securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* Shiro过滤器 - 访问权限控制
* @param securityManager
* @return
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置登录请求url - 注销的url是/logout
shiroFilterFactoryBean.setLoginUrl("/login");
// shiroFilterFactoryBean.setSuccessUrl("/"); // 成功跳转地址
// // 无权限跳转的请求 - 注解鉴权,这个不会生效
// shiroFilterFactoryBean.setUnauthorizedUrl("/403");
/* *
* 过滤链定义
* 设置访问权限 - Map使用LinkedList,因为它是有顺序的
* authc:需要验证的url anno:无需验证的url
*/
Map<String,String> filterChainMap = new LinkedHashMap<>();
// 配置某个url需要某个权限码 - 通常用注解的方式
// filterChainMap.put("/hello", "perms[how_are_you]");
// 过滤掉静态文件css/js/images - Thymeleaf的静态文件一般放在resources/static下的
filterChainMap.put("/css/**","anno");
filterChainMap.put("/js/**","anno");
filterChainMap.put("/images/**","anno");
filterChainMap.put("/lib/**","anno");
// 登录、注册、错误不需要验证
filterChainMap.put("/login","anno");
filterChainMap.put("/register","anno");
filterChainMap.put("/error","anno");
// 需要拦截验证的url
filterChainMap.put("/admin/**","authc");
filterChainMap.put("/user/**","authc");
// 下面这行拦截所有代码必须在Map最后一个,否则会拦截所有url
// 这样写的话,就是除了前面无需验证的,剩下的全都得验证
filterChainMap.put("/**","authc");
// 注意:认证成功/失败也可以通过shiro的过滤器来做 - FormAuthenticationFilter
return shiroFilterFactoryBean;
}
/**
* 凭证匹配器 - 匹配密码的规则
* @return
*/
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 使用MD5算法
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列此时 - 2次
hashedCredentialsMatcher.setHashIterations(2);
// 设置存储的凭证编码,默认为true:即Hex,如果为false,则为Base64编码
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
/************************************** 开启注解配置权限start ****************************************/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
/**
* 默认的代理创建者
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 授权源
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
/************************************** 开启注解配置权限end ****************************************/
/************************************** 开启Redis缓存start ****************************************/
// /**
// * cacheManager 缓存 redis实现
// * 使用的是shiro-redis开源插件
// * @return
// */
// public RedisCacheManager cacheManager() {
// RedisCacheManager redisCacheManager = new RedisCacheManager();
// redisCacheManager.setRedisManager(redisManager());
// return redisCacheManager;
// }
// /**
// * 配置shiro redisManager
// * 使用的是shiro-redis开源插件
// *
// * @return
// */
// @Bean
// public RedisManager redisManager() {
// RedisManager redisManager = new RedisManager();
//// redisManager.setHost(host);
//// redisManager.setPort(port);
//// // 配置缓存过期时间
//// redisManager.setExpire(expireTime);
//// redisManager.setTimeout(timeOut);
//// redisManager.setPassword(password);
// return redisManager;
// }
//
// /**
// * Session Manager
// * 使用的是shiro-redis开源插件
// */
// @Bean
// public DefaultWebSessionManager sessionManager() {
// DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// sessionManager.setSessionDAO(redisSessionDAO());
// return sessionManager;
// }
//
// /**
// * RedisSessionDAO shiro sessionDao层的实现 通过redis
// * 使用的是shiro-redis开源插件
// */
// @Bean
// public RedisSessionDAO redisSessionDAO() {
// RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
// redisSessionDAO.setRedisManager(redisManager());
// return redisSessionDAO;
// }
/************************************** 开启Redis缓存end ****************************************/
}
因为做了密码加密,所以注册的时候也需要对密码加密,如下所示:
@RunWith(SpringRunner.class)
@SpringBootTest
public class Test1 {
@Test
public void test(){
// MD5算法,对123456进行加盐并进行2次散列
String md5Pwd = new SimpleHash("MD5", "123456",
ByteSource.Util.bytes("lcy123456"), 2).toHex();
System.out.println(md5Pwd);
}
}
五、Thymeleaf页面使用Shiro标签
要想在thymeleaf使用shiro的标签,需要引入2.0以上的thymeleaf-extras-shiro
依赖
<!DOCTYPE html>
<!-- 需加入对应的命名空间 -->
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="/logout">注销</a>
<h1 shiro:hasPermission="role:index">role:index</h1>
<h1 shiro:hasPermission="role:indexAbc">role:indexAbc</h1>
<h1 shiro:hasRole="manager">manager</h1>
<h1 shiro:hasRole="abc">abc</h1>
</body>
</html>
六、Controller的书写
Controller层的书写主要是演示手动提交token
和手动注销,以及权限注解RequiresPermissions
的使用。
@Controller
public class ReController {
/**
* 返回JSON数据 - 跳转交给前端来做,这里只是示例
* 但是似乎前端没有办法跳转到templates下的页面
* 根据业务做,可以不返回Json数据,直接进行跳转即可
* 这里因为前端使用的是axios/ajax请求,自己手动提交token
* @param username 账号
* @param password 密码
* @return
*/
@PostMapping("/login")
@ResponseBody
public String Login(@RequestParam("username") String username, @RequestParam("password") String password){
// 从SecurityUtils里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 执行认证登陆
try {
subject.login(token);
} catch (UnknownAccountException uae) {
return "未知账户";
} catch (IncorrectCredentialsException ice) {
return "密码不正确";
} catch (LockedAccountException lae) {
return "账户已锁定";
} catch (ExcessiveAttemptsException eae) {
return "用户名或密码错误次数过多";
} catch (AuthenticationException ae) {
return "用户名或密码不正确!";
}
if (subject.isAuthenticated()) {
return "登录成功";
} else {
token.clear();
return "登录失败";
}
}
/**
* 这个就是针对上面那个返回JSON的反面例子
* 直接跳转到templates下的loginhtml.html
* 这里只是给大家两种思路:可以直接返回页面,也可以让前端来跳转
* @return
*/
@GetMapping("/logout")
public String logout(){
Subject lvSubject=SecurityUtils.getSubject();
// 注销
lvSubject.logout();
return "loginhtml";
}
/**
* templates下的/loginhtml.html页面(登录页面)
* @return
*/
@GetMapping("/")
public String index(){
return "loginhtml";
}
@GetMapping("/index")
public String indexTo(){
return "index";
}
@GetMapping("/admin/add")
@RequiresPermissions("user:list")
public String add(){
return "新增成功!";
}
@GetMapping("/role/index")
@RequiresPermissions("role:index")
public String addRole(){
return "新增角色成功!";
}
@RequiresRoles("admin")
@GetMapping("/admin")
public String admin(){
return "角色admin可以访问!";
]
@RequiresRoles(value = {"admin","user"},logical = Logical.OR)
@GetMapping("/user")
public String user(){
return "拥有admin或user角色可以访问!";
}
}
七、无权限异常处理
@ControllerAdvice
public class ControllerExceptionHandler {
@ExceptionHandler(AuthorizationException.class)
public ModelAndView exceptionHandler(HttpServletRequest request, Exception e){
ModelAndView mv = new ModelAndView();
mv.setViewName("/403");
return mv;
}
}
以上代码总结:
基本演示了前后端分离和非前后端分离的解决方案(不全),对于无权访问也给出了解决方案,演示了Shiro标签在Thymealf页面上的使用。不足的是,因为没有具体的项目做支撑,因此很多地方想的不周全。这里推荐一篇博文:SpringBoot项目+Shiro(权限框架)+Redis(缓存)集成
最后就是:文中所有的return跳转页面,都是跳转的templates下的页面。而使用ajax请求的则是通过前端进行跳转的,则是在static下的页面。
补充:Shiro标签
guest标签
<shiro:guest>
</shiro:guest>
用户没有身份验证时显示相应信息,即游客访问信息。
user标签
<shiro:user>
</shiro:user>
用户已经身份验证/记住我登录后显示相应的信息。
authenticated标签
<shiro:authenticated>
</shiro:authenticated>
用户已经身份验证通过,即Subject.login登录成功,不是记住我登录的。
notAuthenticated标签
<shiro:notAuthenticated>
</shiro:notAuthenticated>
用户已经身份验证通过,即没有调用Subject.login进行登录,包括记住我
自动登录的也属于未进行身份验证。
principal标签
<shiro: principal/>
<shiro:principal property="username"/>
相当于((User)Subject.getPrincipals()).getUsername()。
lacksPermission标签
<shiro:lacksPermission name="org:create">
</shiro:lacksPermission>
如果当前Subject没有权限将显示body体内容。
hasRole标签
<shiro:hasRole name="admin">
</shiro:hasRole>
如果当前Subject有角色将显示body体内容。
hasAnyRoles标签
<shiro:hasAnyRoles name="admin,user">
</shiro:hasAnyRoles>
如果当前Subject有任意一个角色(或的关系)将显示body体内容。
lacksRole标签
<shiro:lacksRole name="abc">
</shiro:lacksRole>
如果当前Subject没有角色将显示body体内容。
hasPermission标签
<shiro:hasPermission name="user:create">
</shiro:hasPermission>
如果当前Subject有权限将显示body体内容
补充 - 使用Shiro的Starter
<!-- 无需添加spring-boot-starter-web,已经依赖了 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.2</version>
</dependency>
用法和上面的基本一致,对于请求的过滤可以在ShiroConfig
使用ShiroFilterChainDefinition
代替ShiroFilterFactoryBean
,示例:
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
// 除了这些,其余的全部都需要验证
chainDefinition.addPathDefinition("/css/**","anno");
chainDefinition.addPathDefinition("/js/**","anno");
chainDefinition.addPathDefinition("/images/**","anno");
chainDefinition.addPathDefinition("/lib/**","anno");
chainDefinition.addPathDefinition("/login","anno");
chainDefinition.addPathDefinition("/register","anno");
return chainDefinition;
}
可在yml中配置Shiro
shiro:
enabled: true # 开启Shiro Web配置,默认为true
loginUrl: /login # 登录地址,默认为/login.jsp
successUrl: /index # 登录成功地址,默认为/
unauthorizedUrl: /unauthorized # 无权限跳转地址
sessionManager:
sessionIdCookieEnabled: true # 是否允许通过URL参数实现会话跟踪,默认为true,如果网站支持Cookie,可以关闭选项
sessionIdUrlRewritingEnabled: true # 是否允许通过Cookie实现会话跟踪