最近在由Spring Boot2.x构建的更简洁的后台管理系统,完美整合SpringMvc + Shiro + MybatisPlus + Beetl技术,项目开发完成会开源出来,希望能对大家学习道路上有所帮助。在这一篇中我将把我整合Shiro过程记录下来,希望对大家的学习这块能有所帮助。
maven依赖包
<!-- shiro框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!--shiro依赖和缓存-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
<version>1.4.0</version>
</dependency>
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鉴权
*
*/
Map<String, String> hashMap = 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
实例
Filter | 解释 |
---|---|
anon | 无参,开放权限,可以理解为匿名用户或游客 |
authc | 无参,需要认证 |
logout | 无参,注销,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url |
authcBasic | 无参,表示 httpBasic 认证 |
user | 无参,表示必须存在用户,当登入操作时不做检查 |
ssl | 无参,表示安全的URL请求,协议为 https |
perms[user] | 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms["user, admin"],当有多个参数时必须每个参数都通过才算通过 |
roles[admin] | 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles["admin,user"],当有多个参数时必须每个参数都通过才算通过 |
rest[user] | 根据请求的方法,相当于 perms[user:method],其中 method 为 post,get,delete 等 |
port[8081] | 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString 其中 schmal 是协议 http 或 https 等等,serverName 是你访问的 Host,8081 是 Port 端口,queryString 是你访问的 URL 里的 ? 后面的参数 |
常用的主要就是 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<String> permissionSet = new HashSet<>();
//用户角色
Set<String> roleNameSet = new HashSet<>();
//获取用户的角色集合
List<Integer> roleList = userInfo.getRoleList();
for (Integer roleId:roleList){
//根据角色id获取到资源信息
List<ModuleOperation> 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<Integer> roleList = ShiroUtils.getUser().getRoleList();
//如果用户不存在角色,跳转到登录界面
if (roleList == null || roleList.size() == 0){
ShiroUtils.getSubject().logout();
model.addAttribute("msg","该用户没有角色,无法登陆");
return "login";
}
//根据角色id查询按钮资源
List<MenuNode> 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";
}
}
测试
密码错误的时候,会自动捕获到异常信息
密码正确,进入到主页面
代码正在编写中,等这块我编写完成,会放到码云上面的
码云地址: https://gitee.com/hrabbit/hrabbit-admin
个人博客:http://www.hrabbit.xin