最近在由Spring Boot2.x构建的更简洁的后台管理系统,完美整合SpringMvc + Shiro + MybatisPlus + Beetl技术,项目开发完成会开源出来,希望能对大家学习道路上有所帮助。在这一篇中我将把我整合Shiro过程记录下来,希望对大家的学习这块能有所帮助。
maven依赖包
。
org.apache.shiro
shiro-spring
1.4.0
org.apache.shiro
shiro-core
slf4j-api
org.slf4j
1.4.0
Shiro 配置类
/** * Shiro配置中心 * * @Auther: hrabbit * @Date: 2018-12-24 12:33 PM
@Description: */ @Configuration public class ShiroConfig {
/**
Shiro的过滤器链
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);/**
- 默认登录路径
*/
shiroFilter.setLoginUrl("/login");/**
- 登录成功后要跳转的链接
/
shiroFilter.setSuccessUrl("/");
/*- 没有权限的时候跳转页面
/
shiroFilter.setUnauthorizedUrl("/global/error");
/*- 配置shiro拦截器链
- anon 不需要认证
- authc 需要认证
- user 验证通过或RememberMe登录的都可以
- 当应用开启了rememberMe时,用户下次访问时可以是一个user,但不会是authc,因为authc是需要重新认证的
- 顺序从上到下,优先级依次降低
- api开头的接口,走rest api鉴权,不走shiro鉴权
*/
MaphashMap = new LinkedHashMap<>();
hashMap.put("/static/", “anon”);
hashMap.put("/login", “anon”);
hashMap.put("/global/sessionError", “anon”);
hashMap.put("/", “user”);
shiroFilter.setFilterChainDefinitionMap(hashMap);
return shiroFilter;
}/**
- 凭证匹配器
- (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
- )
- @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName(“md5”);//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于
md5(md5(""));
return hashedCredentialsMatcher;
}/**
- 自定义shiro认证、授权
- @return
*/
@Bean
public ShiroRealm shiroDbRealm() {
ShiroRealm shiroDbRealm = new ShiroRealm();
shiroDbRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroDbRealm;
} }
注意:里面的 SecurityManager 类导入的应该是 import org.apache.shiro.mgt.SecurityManager;
shirFilter 方法中主要是设置了一些重要的跳转 url,比如未登陆时setLoginUrl,无权限时的跳转setUnauthorizedUrl
权限拦截 Filter
当运行一个Web应用程序时,Shiro将会创建一些有用的默认 Filter 实例,并自动地将它们置为可用,而这些默认的 Filter 实例是被 DefaultFilter 枚举类定义的,当然我们也可以自定义 Filter 实例
常用的主要就是 anon,authc,user,roles,perms 等
注意:anon, authc, authcBasic, user 是第一组认证过滤器,perms, port, rest, roles, ssl 是第二组授权过滤器,要通过授权过滤器,就先要完成登陆认证操作(即先要完成认证才能前去寻找授权) 才能走第二组授权器(例如访问需要 roles 权限的 url,如果还没有登陆的话,会直接跳转到 shiroFilterFactoryBean.setLoginUrl(); 设置的 url
自定义 realm 类
我们首先要继承 AuthorizingRealm 类来自定义我们自己的 realm 以进行我们自定义的身份,权限认证操作。\
/** * 自定义Shiro规则 * @Auther: hrabbit * @Date: 2018-11-21 1:16 PM *
@Description: */ @Slf4j public class MyShiroRealm extends
AuthorizingRealm {@Resource private SysModuleOperationService sysModuleOperationService; @Resource private SysUsersService sysUsersService; @Resource private SysRolesService sysRolesService; /** * 资源认证 * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); ShiroUser userInfo = (ShiroUser) principals.getPrimaryPrincipal(); //按钮资源 Set
permissionSet = new HashSet<>(); //用户角色 Set roleNameSet = new HashSet<>(); //获取用户的角色集合 List roleList = userInfo.getRoleList(); for (Integer roleId:roleList){ //根据角色id获取到资源信息 List allMenuByUserId = sysModuleOperationService.getPermissionByRoleId(roleId); for (ModuleOperation moduleOperation:allMenuByUserId){ if (ToolUtil.isNotEmpty(moduleOperation.getCode())) permissionSet.add(moduleOperation.getCode()); } //查询角色信息 Roles roles = sysRolesService.selectById(roleId); if (roles!=null && ToolUtil.isNotEmpty(roles.getRoleCode())){ roleNameSet.add(roles.getRoleCode()); } } //添加按钮资源 authorizationInfo.addStringPermissions(permissionSet); //添加角色 authorizationInfo.addRoles(roleNameSet); return authorizationInfo; } /** * 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。 * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取shiroFactory工厂 ShiroFactoryService shiroFactory = ShiroFactroy.me(); //获取到用户的信息 UsernamePasswordToken userToken = (UsernamePasswordToken)token; //获取用户的输入的账号. String username = userToken.getUsername(); //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法 ShiroUser userInfo = sysUsersService.getShiroUserByLoginName(username); SysUsers sysUser = sysUsersService.getSysUsersByLoginName(username); //创建缓存用户信息 SimpleAuthenticationInfo info = shiroFactory.info(userInfo,sysUser,super.getName()); return info; } /** * 设置认证加密方式 */ @Override public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) { HashedCredentialsMatcher md5CredentialsMatcher = new HashedCredentialsMatcher(); md5CredentialsMatcher.setHashAlgorithmName(ShiroUtils.hashAlgorithmName); md5CredentialsMatcher.setHashIterations(ShiroUtils.hashIterations); super.setCredentialsMatcher(md5CredentialsMatcher); } }
重写的两个方法分别是实现身份认证以及权限认证,shiro 中有个作登陆操作的 Subject.login()方法,当我们把封装了用户名,密码的 token 作为参数传入,便会跑进这两个方法里面(不一定两个方法都会进入)
其中 doGetAuthorizationInfo方法只有在需要权限认证时才会进去,比如前面配置类中配置了 filterChainDefinitionMap.put("/**", “user”); 的管理员角色,这时进入系统时就会进入 doGetAuthorizationInfo 方法来检查权限;而 doGetAuthenticationInfo 方法则是需要身份认证时(比如前面的 Subject.login()方法)才会进入
再说下 UsernamePasswordToken 类,我们可以从该对象拿到登陆时的用户名和密码(登陆时会使用 new UsernamePasswordToken(username, password);),而 get 用户名或密码有以下几个方法
//获得用户名 String token.getUsername(); //获得用户名 Object
token.getPrincipal(); //获得密码 char[] token.getPassword(); //获得密码 Object
token.getCredentials()
LoginController的实现
/** * 登录控制器 * @Auther: hrabbit * @Date: 2018-11-19 10:23 AM *
@Description: */ @Controller @Slf4j @Api(value = “登录API”,description
= “登录、登出验证,跳转主界面”) public class LoginController extends BaseController {/** * 基础路径 */ private static String BASEURL = "modual"; @Autowired private SysModuleOperationService sysModuleOperationService; @Autowired private SysUsersService sysUsersService; /** * 跳转到主页 * @return */ @RequestMapping(value = {"/","/index"},method = RequestMethod.GET) @ApiOperation(value="跳转到主界面", notes="跳转到主页面,查询用户角色信息和页面信息") public String index(ModelMap model){ //获取用户角色idf List
roleList = ShiroUtils.getUser().getRoleList(); //如果用户不存在角色,跳转到登录界面 if (roleList == null || roleList.size() == 0){ ShiroUtils.getSubject().logout(); model.addAttribute("msg","该用户没有角色,无法登陆"); return "login"; } //根据角色id查询按钮资源 List menuNodes = sysModuleOperationService.getAllMenuByRoleId(roleList); menuNodes = MenuNode.buildTitle(menuNodes); //返回用户资料信息 ShiroUser shiroUser = ShiroUtils.getUser(); //将Shiro用户信息返回到前端页面 model.addAttribute("user",shiroUser); model.addAttribute("title",menuNodes); return BASEURL+"/index.html"; } /** * 跳转到登录界面 * @return */ @RequestMapping(value = "login",method = RequestMethod.GET) @ApiOperation(value="跳转到登录界面", notes="跳转到登录界面") public String login(){ if (ShiroUtils.isAuthenticated() || ShiroUtils.getUser()!=null){ return REDIRECT+ "/"; }else{ return "login.html"; } } /** * 页面提交登录 * * @param username 登录名称 * @param password 用户密码 * @return */ @RequestMapping(value = "/login",method = RequestMethod.POST) @ApiOperation(value="表单验证", notes="提交登录信息") @ApiImplicitParams({ @ApiImplicitParam(name = "username",value = "用户名称",required = true,dataType = "String"), @ApiImplicitParam(name = "password",value = "用户密码",required = true,dataType = "String") }) public String login(String username,String password){ Subject subject = ShiroUtils.getSubject(); //检验用户是否存在 SysUser sysUser = sysUserService.findByLoginName(username); // 在认证提交前准备 token(令牌) UsernamePasswordToken token = new UsernamePasswordToken(username, password); // 执行认证登陆 subject.login(token); ShiroUser shiroUser = ShiroUtils.getUser(); //将ShiroUser对象存储到session中 HttpUtils.getRequest().getSession().setAttribute("shiroUser",shiroUser); //保存Session状态 ShiroUtils.getSession().setAttribute("sessionFlag",true); return REDIRECT+"/"; } /** * 退出登录 * @return */ @RequestMapping(value = "loginOut",method = RequestMethod.GET) @ApiOperation(value="退出登录", notes="返回登录界面") public String loginOut(){ ShiroUtils.getSubject().logout(); return REDIRECT+"login.html"; } }
这里我们需要注意创建异常拦截器,这样当用户名或者密码不正确的时候,Shiro会自动抛出异常,我们只需要将异常捕获即可
/** * 异常类 * * @Auther: hrabbit * @Date: 2018-11-15 3:40 PM *
@Description: */ @ControllerAdvice(“com.hrabbit.admin”) @Order(-1)
@Slf4j public class GlobalExceptionHandler {/** * 其他异常抛出信息 * * @param response * @param ex * @return */ @ExceptionHandler(Exception.class) public BaseResponse otherExceptionHandler(HttpServletResponse response, Exception ex) { response.setStatus(500); log.error(ex.getMessage(), ex); return new BaseResponse(500, ex.getMessage()); } /** * 账号被冻结异常 */ @ExceptionHandler(DisabledAccountException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public String accountLocked(DisabledAccountException e, Model model) { model.addAttribute("message", "账号被冻结"); return "/login.html"; } /** * 账号密码错误异常 */ @ExceptionHandler(CredentialsException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public String credentials(CredentialsException e, Model model) { model.addAttribute("message", "账号密码错误"); return "/login.html"; } }
测试
密码错误的时候,会自动捕获到异常信息
感兴趣的可以自己来我的Java架构群,可以获取免费的学习资料,群号:798891710对Java技术,架构技术感兴趣的同学,欢迎加群,一起学习,相互讨论。