本文使用SpringBoot2.0+ MyBatis + Shiro 实现一个简单的安全管理。对集成MyBatis可以参考文章 Spring Boot 整合MyBatis
添加依赖
<dependencies>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--MyBatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--MySQL-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--Shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
创建实体类和数据访问服务
创建3个实体类:User、 Role、Permission
@Data
public class User implements Serializable {
private static final long serialVersionUID = -8315794285126194641L;
private Integer id;
private String userName;
private String password;
private String status;
}
@Data
public class Role implements Serializable {
private static final long serialVersionUID = -7713029747061173171L;
private Integer id;
private String name;
private String memo;
}
@Data
public class Permission implements Serializable {
private static final long serialVersionUID = -316747523328447976L;
private Integer id;
private String url;
private String name;
}
数据访问层
@Component
public interface UserMapper {
/**
* 根据用户名查询对应的用户信息
*
* @param userName
* @return
*/
User findByUserName(String userName);
}
@Component
public interface UserPermissionMapper {
/**
* 根据用户名查询用户所拥有的操作权限集合
*
* @param username
* @return
*/
List<Permission> findPermsByUsername(String username);
}
@Component
public interface UserRoleMapper {
/**
* 查询用户所属的角色列表
*
* @param username
* @return
*/
List<Role> findRolesByUsername(String username);
}
然后创建一个UserService服务,用来读取用户,角色等信息
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private UserPermissionMapper userPermissionMapper;
/**
* 根据用户名查找对应的用户信息
*
* @param userName
* @return
*/
public User findByUserName(String userName) {
return userMapper.findByUserName(userName);
}
/**
* 根据用户名查找对应的角色集合
*
* @param username
* @return
*/
public List<Role> findRolesByUsername(String username) {
return userRoleMapper.findRolesByUsername(username);
}
/**
* 根据用户名查找对应的操作权限集合
*
* @param username
* @return
*/
public List<Permission> findPermsByUsername(String username) {
return userPermissionMapper.findPermsByUsername(username);
}
}
访问sql语句的xml文件
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaozhao.dao.UserMapper">
<resultMap type="com.xiaozhao.domain.User" id="User">
<id column="id" property="id" javaType="java.lang.Integer" jdbcType="INTEGER"/>
<result column="username" property="userName" javaType="java.lang.String" jdbcType="VARCHAR"/>
<result column="password" property="password" javaType="java.lang.String" jdbcType="VARCHAR"/>
<result column="status" property="status" javaType="java.lang.String" jdbcType="VARCHAR"/>
</resultMap>
<select id="findByUserName" resultMap="User">
select * from t_user where username = #{userName}
</select>
</mapper>
UserRoleMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaozhao.dao.UserRoleMapper">
<resultMap type="com.xiaozhao.domain.Role" id="RoleMap">
<id column="id" property="id" javaType="java.lang.Integer" jdbcType="INTEGER"/>
<result column="memo" property="memo" javaType="java.lang.String" jdbcType="VARCHAR"/>
<result column="name" property="name" javaType="java.lang.String" jdbcType="VARCHAR"/>
</resultMap>
<select id="findRolesByUsername" resultMap="RoleMap">
select r.id,r.name,r.memo from t_role r
left join t_user_role ur on ur.rid = r.id
left join t_user u on u.id = ur.user_id where u.username= #{userName}
</select>
</mapper>
UserPermission.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaozhao.dao.UserPermissionMapper">
<resultMap type="com.xiaozhao.domain.Permission" id="PermissionMap">
<id column="id" property="id" javaType="java.lang.Integer" jdbcType="INTEGER"/>
<result column="url" property="url" javaType="java.lang.String" jdbcType="VARCHAR"/>
<result column="name" property="name" javaType="java.lang.String" jdbcType="VARCHAR"/>
</resultMap>
<select id="findPermsByUsername" resultMap="PermissionMap">
select p.id,p.url,p.name from t_role r
left join t_user_role ur on(r.id = ur.rid)
left join t_user u on(u.id = ur.user_id)
left join t_role_permission rp on(rp.rid = r.id)
left join t_permission p on(p.id = rp.pid )
where u.username = #{username}
</select>
</mapper>
配置Shiro
SSM架构下都是配置在xml文件中,SpringBoot通过代码来进行配置
新建一个ShiroConfig的类,代码如下:
@Configuration
public class ShiroConfig {
/**
* 配置过滤器链,过滤器配置要求是有顺序的,并且是短路优先原则,即前面的匹配到之后,就不再执行后面的过滤器了
* 1)注入SecurityManager
* 2)配置过滤器规则
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
LinkedHashMap<String, String> filterChainMap = new LinkedHashMap<>(12);
// 静态资源不予拦截
filterChainMap.put("/css/**", "anon");
filterChainMap.put("/js/**", "anon");
filterChainMap.put("/fonts/**", "anon");
filterChainMap.put("/img/**", "anon");
// 退出
filterChainMap.put("/logout", "anon");
// 退出
filterChainMap.put("/", "anon");
// 除了以上url外,其他的都需要认证通过才可以访问,否则转向login
filterChainMap.put("/**", "authc");
// TODO 未能拦截到403
// filterChainMap.put("/user/delete", "perms[user:delete]");
// 登录url
shiroFilterFactoryBean.setLoginUrl("/login");
// 未授权url
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
// 登录成功后的跳转
shiroFilterFactoryBean.setSuccessUrl("/index");
// 设置拦截规则
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
return shiroFilterFactoryBean;
}
// ----------------------------------------开启注解 BEGIN--------------------------------------------
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 启用角色,权限注解,例如:@RequiresPermissions,@RequiresRoles
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
//----------------------------------------开启注解 END--------------------------------------------
/**
* 配置SecurityManager,核心
*
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
return securityManager;
}
/**
* 自定义Realm,获取用户的安全数据。机制就是把前端传递的用户名和密码和数据库中的信息比对,验证登录是否合法,并且得到登录用户的角色信息,权限信息
* 1)登录验证
* 2)角色,权限校验
*
* @return
*/
@Bean
public ShiroRealm shiroRealm() {
return new ShiroRealm();
}
/**
* Shiro生命周期处理器
*
* @return
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
然后再新建一个自定义的Realm,这里新建一个ShiroRealm的类,代码如下:
public class ShiroRealm extends AuthorizingRealm {
/**
* 一个服务,可以从数据库查询用户信息,包含用户名和密码,查询用户所属的角色,用户所拥有的权限
*/
@Autowired
private UserService userService;
/**
* 验证当前登录用户的角色信息,权限信息
* 把当前登录客户的角色信息集合,可操作权限集合从数据库中取出来,
* 然后保存到SimpleAuthorizationInfo对象中,并返回给Shiro,这样Shiro中就存储了当前用户的角色和权限信息了
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) SecurityUtils.getSubject().getPrincipal();
String username = user.getUserName();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
System.out.println("角色校验,获取权限" + username);
// 获取用户角色集
List<Role> roleList = userService.findRolesByUsername(username);
Set<String> roleSet = new HashSet<>(roleList.size());
for (Role role : roleList) {
roleSet.add(role.getName());
}
info.setRoles(roleSet);
// 获取用户权限集
List<Permission> permissionList = userService.findPermsByUsername(username);
Set<String> permissionSet = new HashSet<>(permissionList.size());
for (Permission permission : permissionList) {
permissionSet.add(permission.getName());
}
info.setStringPermissions(permissionSet);
return info;
}
/**
* 登录验证
* 根据客户端传递的登录信息,然后从数据库中取出对应的用户录,进行比对,看是否匹配
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
/**
* 从AuthenticationToken中获取到前端传递的用户名和密码字段信息
*/
String username = (String) token.getPrincipal();
String password = new String((char[]) token.getCredentials());
System.out.println("认证用户:" + username + "---" + password);
/**
* 从数据库中查询对应的用户记录
*/
User user = this.userService.findByUserName(username);
/**
* 依次对没有查询到记录,密码错误,账号锁定等信息进行处理,然后抛出对应的异常
*/
if (user == null) {
throw new UnknownAccountException("用户名或密码错误");
}
if (!password.equals(user.getPassword())) {
throw new IncorrectCredentialsException("用户名或密码错误");
}
if (user.getStatus().equals("0")) {
throw new LockedAccountException("账号已被锁定,请联系管理员");
}
/**
*
*/
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
return info;
}
}
开发界面
登录功能
新建一个LoginController
@Controller
public class LoginController {
/**
* 查询用户信息服务
*/
@Autowired
private UserService userService;
/**
* 返回登录页面
*
* @return
*/
@GetMapping("/login")
public String login() {
return "login";
}
/**
* 测试方法,得到一个用户数据,返回json格式
*
* @return
*/
@GetMapping("/user")
@ResponseBody
public User gerUser() {
return userService.findByUserName("mrbird");
}
/**
* 跳转首页
*
* @param model
* @return
*/
@GetMapping("/index")
public String index(Model model) {
User user = (User) SecurityUtils.getSubject().getPrincipal();
model.addAttribute("user", user);
return "index";
}
/**
* 默认情况下跳转首页
*
* @return
*/
@GetMapping("/")
public String redirectIndex() {
return "redirect:/index";
}
/**
* 登录方法,post形式
*
* @param username
* @param password
* @return
*/
@PostMapping("/login")
@ResponseBody
public HttpResult login(String username, String password) {
String encPassword = MD5Utils.encrypt(username, password);
UsernamePasswordToken token = new UsernamePasswordToken(username, encPassword);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
return HttpResult.ok();
} catch (UnknownAccountException e) {
return HttpResult.error(e.getMessage());
} catch (IncorrectCredentialsException e) {
return HttpResult.error(e.getMessage());
} catch (LockedAccountException e) {
return HttpResult.error(e.getMessage());
} catch (AuthenticationException e) {
return HttpResult.error("认证失败");
}
}
/**
* 没有权限跳转的界面
*
* @return
*/
@GetMapping("/403")
public String forbid() {
return "403";
}
/**
* 用户注销登录
*
* @return
*/
@GetMapping("/logout")
public String logout() {
SecurityUtils.getSubject().logout();
return "redirect:/index";
}
}
然后在resources>templates文件夹下新建一个login.html和index.html
运行之后,地址栏输入http://localhost:8080 进行访问时,都会重新定位到登录页面,登录成功之后,会跳转到index页面。
Shiro为我们提供了一些和权限相关的注解,如下所示:
// 表示当前Subject已经通过login进行了身份验证;即Subject.isAuthenticated()返回true。
@RequiresAuthentication
// 表示当前Subject已经身份验证或者通过记住我登录的。
@RequiresUser
// 表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
@RequiresGuest
// 表示当前Subject需要角色admin和user。
@RequiresRoles(value={"admin", "user"}, logical= Logical.AND)
// 表示当前Subject需要权限user:a或user:b。
@RequiresPermissions (value={"user:a", "user:b"}, logical= Logical.OR)
添加一个UserController
Controller
@RequestMapping("/user")
public class UserController {
/**
* 需要用户有list操作权限
*
* @param model
* @return
*/
@RequiresPermissions("user:list")
@RequestMapping("list")
public String userList(Model model) {
model.addAttribute("value", "获取用户信息");
return "user";
}
/**
* 需要用户有add操作权限
*
* @param model
* @return
*/
@RequiresPermissions("user:add")
@RequestMapping("add")
public String userAdd(Model model) {
model.addAttribute("value", "新增用户");
return "user";
}
/**
* 需要用户有delete操作权限
*
* @param model
* @return
*/
@RequiresPermissions("user:delete")
@RequestMapping("delete")
public String userDelete(Model model) {
model.addAttribute("value", "删除用户");
return "user";
}
}
代码示例
https://github.com/xiaozhaowen/spring-boot-in-action/tree/master/springboot-shiro