六 SpringBoot集成Shiro认证
1 分析
Shiro提供认证授权功能,所以SpringBoot中不需再编写自定义注解,权限拦截,登录拦截,登录登出。Shiro 环境中有三个封装对象Subject ,SecurityManager和Realms,SpringBoot 集成 Shiro 时需要配置相对应的Bean(Subject 不用)
2 导入依赖
<properties><java.version>8</java.version><shiro.version>1.7.1</shiro.version><thymeleaf.extras.shiro.version>2.0.0</thymeleaf.extras.shiro.version>
</properties>
<!--Shiro核心框架 -->
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId><version>${shiro.version}</version>
</dependency>
<!-- Shiro使用Spring框架 -->
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>${shiro.version}</version>
</dependency>
<!-- Shiro使用EhCache缓存框架 -->
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-ehcache</artifactId><version>${shiro.version}</version>
</dependency>
<!-- thymeleaf模板引擎和shiro框架的整合 -->
<dependency><groupId>com.github.theborakompanioni</groupId><artifactId>thymeleaf-extras-shiro</artifactId><version>${thymeleaf.extras.shiro.version}</version>
</dependency>
3 创建数据源
// 有类才能生成Bean
public class EmployeeRealm extends AuthorizingRealm {@Autowiredprivate IEmployeeService employeeService;@Autowiredprivate IPermissionService permissionService;@Autowiredprivate IRoleService roleService;//授权方法@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {Employee currentEmployee= (Employee) principalCollection.getPrimaryPrincipal();SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();if(currentEmployee.isAdmin()){List<Role> roles=roleService.listAll();for(Role role:roles){info.addRole(role.getSn());}info.addStringPermission("*:*");}else{List<Role> roleList=roleService.queryByEmployeeId(currentEmployee.getId());for(Role role:roleList){info.addRole(role.getSn());}//查询该用户的权限集合List<String> permissionList=permissionService.queryByEmployeeId(currentEmployee.getId());info.addStringPermissions(permissionList);}return info;}//认证方法@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {// 根据token获取用户名String username = (String) authenticationToken.getPrincipal();// 根据用户名查询用户Employee currentEmployee=employeeService.getByUsername(username);// 根据查询结果返回对应数据if(currentEmployee==null){return null;}return new SimpleAuthenticationInfo(currentEmployee,currentEmployee.getPassword(), ByteSource.Util.bytes(currentEmployee.getSalt()),getName());}
}
4 创建Shiro配置类
// 配置类注解
@Configuration
public class ShiroConfig {// 1.Realm 数据源从数据库中查询数据(先有Realm才能配置Bean,配置这个Bean需要先有这个类)// Bean一定是对象,对象不一定是Bean,对象需要基于类创建@Beanpublic EmployeeRealm employeeRealm(){EmployeeRealm realm = new EmployeeRealm();return realm;}// 2.SecurityManager 安全管理器(基于web环境下的)// 此处可用set调本类方法或传参的方式联系Realm // 传参是在spring容器中查找这个Bean先类型再名字,去掉@Bean注解会报错(参数名与方法名尽量一致)// 调用方法首先不会运行该方法,会看方法的返回值类型,在容器中查找该类型,找到多个再按照名字去找,找到了就直接用// 不会运行该方法,若在容器中没找到该方法,就运行该方法并把返回值放到容器中,然后再拿过来用(无@Bean注解也行)@Beanpublic DefaultWebSecurityManager defaultWebSecurityManager(EmployeeRealm employeeRealm){DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(employeeRealm);return securityManager;}// 请求拦截器 shiro过滤器 由于创建麻烦此处使用工厂类创建过滤器对象// 若想知道一个工厂类返回什么类型的Bean 可查询其getObject()方法@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();// 配置登录页面shiroFilterFactoryBean.setLoginUrl("/static/login.html");// 配置安全管理器shiroFilterFactoryBean.setSecurityManager(securityManager);// 配置拦截规则(过滤链) 底层为双链表组成(有序,就是我们放入的顺序,过滤器根据顺序执行)LinkedHashMap<String,String> filterChainDefinitionMap=new LinkedHashMap<>();// 对静态资源设置匿名访问(浏览器图标 html css js)放行filterChainDefinitionMap.put("/favicon.ico**","anon");filterChainDefinitionMap.put("/static/**","anon");// 不需拦截的访问(公共资源)放行filterChainDefinitionMap.put("/login","anon");// 退出并且shiro清除session信息(上下文对象也就是用户信息) 执行退出方法// 无需在再编写退出方法,直接调用logout即可踢回登陆页面filterChainDefinitionMap.put("/logout","logout");// 进行拦截filterChainDefinitionMap.put("/**","authc");// 将拦截规则设置给拦截器链(shiro生成了很多拦截器,看我们选用哪个)shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);// 此时无登录信息 访问任何页面都应该被踢回登录页面return shiroFilterFactoryBean;}
}
5 LoginController – 登录方法
登录认证实际上是shiro在做,但数据需要在realm中提供
@Controller
public class LoginController {@RequestMapping("/login")@ResponseBodypublic JsonResult login(String username, String password){// 他会自动从Spring容器中拿到SecurityManager设置给SecurityUtils// 然后再将SecurityManager设置给subjectSubject subject = SecurityUtils.getSubject();// 将用户名密码封装到tokenUsernamePasswordToken token = new UsernamePasswordToken(username,password);// 此处返回异常不精确到某一项,防止有人试错(先试帐号再试密码)try {// 登录失败就抛异常subject.login(token);subject.getSession().setAttribute("user_in_session",subject.getPrincipal());} catch (UnknownAccountException e) {return new JsonResult(false,"账号密码有误");} catch (IncorrectCredentialsException e) {return new JsonResult(false,"帐号密码有误");} catch (Exception e) {return new JsonResult(false,"系统异常,稍后再试");}return new JsonResult(true,"登录成功");}
}
6 数据源查询方法(service) – getByUsername
// IEmployeeService
Employee getByUsername(String username);
// EmployeeServiceImpl
public Employee getByUsername(String username) {return employeeMapper.getByUsername(username);
}
7 数据源查询方法(mapper) – getByUsername
// mapper
Employee getByUsername(String username);
// xml
<select id="getByUsername" resultMap="BaseResultMap">select e.id, e.username, e.name, e.password, e.email, e.age, e.admin,d.id d_id,d.name d_name,d.sn d_sn,e.saltfrom employee e left join department d on e.dept_id = d.idwhere username=#{username}
</select>
8 shiro 内置过滤器
shiro 启动时会默认将以下这些类(过滤器)加载到程序中,然后使用Map将这些数据的关系以key value的方式存储起来。
过滤器的名称(key) | Java 类(value) |
---|---|
anon | org.apache.shiro.web. lter.authc.AnonymousFilter |
authc | org.apache.shiro.web. lter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web. lter.authc.BasicHttpAuthenticationFilter |
roles | org.apache.shiro.web. lter.authz.RolesAuthorizationFilter |
perms | org.apache.shiro.web. lter.authz.PermissionsAuthorizationFilter |
user | org.apache.shiro.web. lter.authc.UserFilter |
logout | org.apache.shiro.web. lter.authc.LogoutFilter |
port | org.apache.shiro.web. lter.authz.PortFilter |
rest | org.apache.shiro.web. lter.authz.HttpMethodPermissionFilter |
ssl | org.apache.shiro.web. lter.authz.SslFilter |
anon: 匿名拦截器,即不需要登录即可访问(谁都可以访问,不需要拦截);一般用于静态资源过滤;示例“/static/**=anon”
authc: 表示需要认证(登录)才能使用,该路径所有请求都需登录后才能访问;示例“/**=authc”
authcBasic:Basic HTTP身份验证拦截器
roles: 角色授权拦截器,验证用户是否拥有资源角色;示例“/admin/**=roles[admin]”
perms: 权限授权拦截器,验证用户是否拥有资源权限;示例“/user/create=perms[“user:create”]”
user: 用户拦截器,用户已经身份验证/记住我登录的都可;示例“/index=user”
logout: 退出拦截器,登出后自动清理session中的用户信息。主要属性:redirectUrl:退出成功后重定向的地址(/);示例“/logout=logout”
port: 端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样
rest: rest风格拦截器;
ssl: SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样;
9 400错误问题解决
当传入的参数 SpringMVC 无法转换时,就会出现400问题(第一次访问时出现),session是在浏览器第一次访问服务器时,由服务器创建并生成一个sessionID,通过response响应给浏览器。
浏览器访问服务器时,服务器中有一个session池,当找到session后,将返回对应的JsessionID。
但第一次访问时会首先经过shiro过滤器,其中有一个会话管理器(SessionManager),他发现当前是第一次访问,因此会进行一次URL重写,服务器会生成session并把sessionID返回给安全管理器,会话管理器通过重定向回到浏览器,再次发起申请访问服务器,此时第一次访问服务器实际上是没有访问到服务器,因此服务器无法接收携带的参数(JsessionID)报400错误。(去掉url中的JsessionID即可访问)
第二次访问时,session已经存在,通过id寻找session,可以正常访问。或者告诉会话管理器,不需要做url重写,可在shiroconfig中生成会话管理器,将 setSessionIdUrlRewritingEnabled 设置为false即可(默认为true)。
@Configuration
public class ShiroConfig {// 略// 会话管理器@Beanpublic DefaultWebSecurityManager sessionManager(){DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();// url重写开关sessionManager.setSessionIdUrlRewritingEnabled(false);return sessionManager;}
}
此处需注意,编写好 sessionManager 会话管理器后,需要将其设置给 SecurityManager 安全管理器(通过参数)
@Configuration
public class ShiroConfig {// 略@Bean//安全管理器public DefaultWebSecurityManager defaultWebSecurityManager(EmployeeRealm employeeRealm, DefaultWebSessionManager sessionManager){// 创建安全管理器DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();// 设置realmsecurityManager.setRealm(employeeRealm);// 设置会话管理器securityManager.setSessionManager(sessionManager);return securityManager;}//会话管理器@Beanpublic DefaultWebSecurityManager sessionManager(){DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();sessionManager.setSessionIdUrlRewritingEnabled(false);return sessionManager;}
}