功能:解决web站点的登录,权限验证,授权等功能
优点:在不影响站点业务代码,可以权限的授权与验证横切到业务中
比较:shiro比Security 划分更细化
项目使用思考:
1:授权中用户的权限。变动不大,可以存在缓存中,不用每次都去数据库中查一次
2:ShiroFilterFactoryBean 中要验证的权限,由于只在程序初始化的时候执行。后期对角色或权限修改,还要重启项目。(是否要在更新或修改权限的时候,动态更新问题)
依赖包:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.6.0</version> </dependency> <!--thymeleaf整合shiro--> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
PS: 用户的授权信息,最好存入缓存中,不用每次都从缓存中取。本实例中未用缓存
public class MyRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private RoleService roleservice; @Autowired private PermissionService permissionService; /** * 授权(为当前登录成功的用户授予权限和分配角色) * * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 1、获取用户 User user = (User) principals.getPrimaryPrincipal(); if (user == null) { log.warn("授权失败,用户信息异常,用户{}", this.getClass().getName()); return authorizationInfo; } // 2、根据用户名去数据库中查询用户信息(查询该认证用户下角色/权限信息) List<Role> roleList = roleservice.getRoleListByUserName(user.getUserName()); List<Permission> permissionList=null; if(roleList.size()>0){ for(Role role:roleList){ //添加角色 authorizationInfo.addRole(role.getRole());//添加角色名称(要唯一) permissionList=permissionService.getPermissionListByRoleId(role.getRoleId()); for(Permission permission : permissionList){ //添加权限 authorizationInfo.addStringPermission(permission.getPermission());//添加权限(user:add) } } } log.info("====doGetAuthorizationInfo注册完成===="); return authorizationInfo; } /** * 认证、登录(用来验证当前登录的用户,获取认证信息) * * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //1.把AuthenticationToken类型的token转换为UsernamePasswordToken(封装了从前端传递的用户名和密码(用户输入)) UsernamePasswordToken userToken=(UsernamePasswordToken)token; //2.通过UsernamePasswordToken获取username String username = userToken.getUsername(); if(!StringUtils.hasText(username)){ return null; } // 3.通过username查询数据库,且判断数据库中的用户状态 User user= userService.findByUserName(username); if(user==null){ throw new UnknownAccountException("该用户不存在"); } if(user.getState()==0){ throw new AuthenticationException("创建未认证"); } if(user.getState()==2){ throw new LockedAccountException("该用户被锁定"); } // 4.传入用户名和密码进行身份认证,并返回认证信息(盐值可以不传【加盐后相同密码加密后不一致】) // 认证的第一个参数 可以是Username也可以是User实体类对象 // 如果传的参数为username,那么在授权阶段,使用principals.getPrimaryPrincipal();获取到的就是Username // 如果传的参数为user对象,那么在授权阶段,获取到的就是user对象 AuthenticationInfo authcInfo = new SimpleAuthenticationInfo( user, user.getPassword(), ByteSource.Util.bytes("1"), this.getClass().getName() ); log.info("====doGetAuthenticationInfo注册完成===="); return authcInfo; } }
PS:filterChainMap.put("/user/**", "authc"); //过滤链定义,从上向下顺序执行,一般将 /**放在最为下边(否则可能导致 授权不会执行)
@Configuration @Slf4j public class ShiroConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.password}") private String password; @Value("${spring.redis.timeout}") private int timeout; /** * 注入自定义的 Realm [将自己的验证方式加入容器] * * @return */ @Bean public MyRealm myAuthRealm() { MyRealm myRealm = new MyRealm();//配置自定义密码比较器 myRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myRealm; } /** * 注入安全管理器(配置 SecurityManager 时,需要将上面自定义 Realm 添加进来,这样 Shiro 才可访问该 Realm) * * @return */ @Bean public SecurityManager securityManager() { // 将自定义 Realm 加进来 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(myAuthRealm()); log.info("====securityManager注册完成===="); //PS:DefaultWebSecurityManager无法转SecurityManager,需要手动导入此包(import org.apache.shiro.mgt.SecurityManager;) return securityManager; } @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, PermissionService permissionService) { // 定义 shiroFactoryBean ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl("/user/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/user/index"); // 未授权界面(认证不通过跳转); shiroFilterFactoryBean.setUnauthorizedUrl("/user/403"); // 拦截器(LinkedHashMap 是有序的,进行顺序拦截器配置) Map<String, String> filterChainMap = new LinkedHashMap<>(); // 配置不会被拦截的链接 顺序判断【anon表示放行】 filterChainMap.put("/static/**", "anon"); filterChainMap.put("/public", "anon"); // 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了 filterChainMap.put("/user/logout", "logout"); //添加要检查的权限(如果改动在数据库中添加或删除权限,不重启程序,权限是不会生效的。需要动态配置) List<Permission> permissionList = permissionService.getAllPermissionList(); for (Permission permission : permissionList) { filterChainMap.put(permission.getUrl(), "perms[" + permission.getPermission() + "]"); } //filterChainMap.put("/user/userAdd", "perms[user:add]"); //filterChainMap.put("/user/userDel", "perms[user:del]"); // 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 // authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问 filterChainMap.put("/user/**", "authc"); // 设置 shiroFilterFactoryBean 的 FilterChainDefinitionMap shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap); log.info("====shiroFilterFactoryBean注册完成===="); return shiroFilterFactoryBean; }/** * 开启shiro 注解模式 * 可以在controller中的方法前加上注解 * 如 @RequiresPermissions("userInfo:add") * * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; }
/**
* 凭证匹配器(密码校验交给Shiro的SimpleAuthenticationInfo进行处理)
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
//使用MD5加密
// Md5Hash md5 = new Md5Hash("admin", "1", 2);
// log.info("====求得密码:"+md5+"====");
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 加密方式
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 加密两次
hashedCredentialsMatcher.setHashIterations(2);
// 是否存储为16进制
//hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
}
PS:注意权限更新后,动态更新权限(ShiroFilterFactoryBean 只在初始化的时候执行,新增或修角色改权限时,需要动态更新)
@Controller @Slf4j @RequestMapping("/user") public class UserController {
//获取数据空的权限 @Autowired ShiroService shiroService;
/** * 修改数据库中的权限后,动态更新权限 * @return */ @RequestMapping("/updatePermission") public String updatePermission(){ shiroService.updatePermission(); return "redirect:/user/userList"; } @RequestMapping("/index2") public String index2(){ return "/shiro/index"; } @RequestMapping("/index") public String index(){ return "/shiro/index"; } @RequestMapping("/403") public String unauthorized(){ return "/shiro/unauthorized"; } @RequestMapping("/logout") public String logout(){ // 获取 subject 认证主体 Subject subject = SecurityUtils.getSubject(); subject.logout(); return "redirect:/user/login"; } @RequestMapping("/login") public String login(){ return "/shiro/login"; } @RequestMapping(value = "/login",method = RequestMethod.POST) public String login(User user, HttpServletRequest request, RedirectAttributes redirectAttributes, Model model){ //RedirectAttributes : 用于重定向带参数 if (user.getUserName()== null || user.getUserName().isEmpty()) { redirectAttributes.addFlashAttribute("message", "用戶名不能爲空"); return "redirect:/user/login"; } // 获取 subject 认证主体 Subject currentUser = SecurityUtils.getSubject(); //currentUser.isAuthenticated()当前用户是否被认证 //如果当前用户被认证了,说明用户还是处于登录状态 if(!currentUser.isAuthenticated()) { // 把需要登录的用户名和密码存入shiro提供的令牌中 UsernamePasswordToken token=new UsernamePasswordToken(user.getUserName(),user.getPassword()); try { // subject调用login方法来进行匹配用户是否可以登录成功 // login方法的参数需要接收shiro的UsernamePasswordToken类型 currentUser.login(token); //Session session = currentUser.getSession(); // 设置会话session //session.setAttribute("userName", user.getUserName()); //登录成功写入Session等其他操作 //request.getSession().setAttribute("user",user); return "/shiro/index"; } catch(UnknownAccountException uae){ log.info("对用户[" + user.getUserName() + "]进行登录验证..验证未通过,未知账户"); redirectAttributes.addFlashAttribute("message", "未知账户"); }catch(IncorrectCredentialsException ice){ log.info("对用户[" + user.getUserName() + "]进行登录验证..验证未通过,错误的凭证"); redirectAttributes.addFlashAttribute("message", "密码不正确1"); }catch(LockedAccountException lae){ log.info("对用户[" + user.getUserName() + "]进行登录验证..验证未通过,账户已锁定"); redirectAttributes.addFlashAttribute("message", "账户已锁定"); }catch(AuthenticationException ae){ //通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景 log.info("对用户[" + user.getUserName() + "]进行登录验证..验证未通过,堆栈轨迹如下"); ae.printStackTrace(); redirectAttributes.addFlashAttribute("message", "用户名或密码不正确"); } return "redirect:/user/login"; } //Tis:return 中 redirect:后面是控制器的路由地址,不是页面地址 return "redirect:/user/index"; //return "/shiro/index"; } }
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<h3 shiro:principal>登录人</h3> <shiro:hasPermission name="user:add"> <a th:href="@{/add}" >添加</a> </shiro:hasPermission> <shiro:hasPermission name="user:del"> <a th:href="@{/del}" >删除</a> </shiro:hasPermission>