六 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 {
@Autowired
private IEmployeeService employeeService;
@Autowired
private IPermissionService permissionService;
@Autowired
private IRoleService roleService;
//授权方法
@Override
protected 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;
}
//认证方法
@Override
protected 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,对象需要基于类创建
@Bean
public EmployeeRealm employeeRealm(){
EmployeeRealm realm = new EmployeeRealm();
return realm;
}
// 2.SecurityManager 安全管理器(基于web环境下的)
// 此处可用set调本类方法或传参的方式联系Realm
// 传参是在spring容器中查找这个Bean先类型再名字,去掉@Bean注解会报错(参数名与方法名尽量一致)
// 调用方法首先不会运行该方法,会看方法的返回值类型,在容器中查找该类型,找到多个再按照名字去找,找到了就直接用
// 不会运行该方法,若在容器中没找到该方法,就运行该方法并把返回值放到容器中,然后再拿过来用(无@Bean注解也行)
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(EmployeeRealm employeeRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(employeeRealm);
return securityManager;
}
// 请求拦截器 shiro过滤器 由于创建麻烦此处使用工厂类创建过滤器对象
// 若想知道一个工厂类返回什么类型的Bean 可查询其getObject()方法
@Bean
public 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")
@ResponseBody
public JsonResult login(String username, String password){
// 他会自动从Spring容器中拿到SecurityManager设置给SecurityUtils
// 然后再将SecurityManager设置给subject
Subject subject = SecurityUtils.getSubject();
// 将用户名密码封装到token
UsernamePasswordToken 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.salt
from employee e left join department d on e.dept_id = d.id
where 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 {
// 略
// 会话管理器
@Bean
public 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();
// 设置realm
securityManager.setRealm(employeeRealm);
// 设置会话管理器
securityManager.setSessionManager(sessionManager);
return securityManager;
}
//会话管理器
@Bean
public DefaultWebSecurityManager sessionManager(){
DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
}