Spring Boot -- Apache Shiro
1. pom.xml
shiro并没有提供对应的Starter,而是使用的shiro-spring,其它的依赖都是辅助
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- spring-data-jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
2. application.properties
#数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/jpa_test?useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#jpa配置
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.show-sql=true
logging.level.com.example.demo.jpa.repository=debug
3. entity
用户-角色-权限
一个用户可以拥有多个角色,同一个角色也可以赋给多个用户,所以是用户和角色属于多对多关系,
一个角色可以有多种权限,一种权限属于多个角色,角色和权限也属于多对多关系,多对多需要中间表,多对多使用@ManyToMany标注,中间表使用@JoinTable来标注,使用joinColumns来指定连接列的名称,使用inverseJoinColumns来指定被连接的列的名称
用户表
@Entity
public class UserInfo {
@Id
@GeneratedValue
private Integer uid;
@Column(unique = true)
private String username;
private String password;
private String name;
//用于加密的字符串,保证密码生成的MD5的唯一性
private String salt;
//0:未认证,1:正常状态,2:用户被锁定
private byte state;
//立即从数据库中进行加载数据
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "SysUserRole", joinColumns = {@JoinColumn(name = "uid")}, inverseJoinColumns = {@JoinColumn(name = "roleId")})
private List<SysRole> sysRoles;
//get/set
}
角色表
@Entity
public class SysRole {
@Id
@GeneratedValue
private Integer id;
private String role;
private String description;
private Boolean available = Boolean.FALSE;
@ManyToMany
@JoinTable(name = "SysUserRole", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "uid")})
private List<UserInfo> userInfos;
@ManyToMany
@JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "permissionId")})
private List<SysPermission> sysPermissions;
//get/set
}
权限表
@Entity
public class SysPermission {
@Id
@GeneratedValue
private Integer id;
private String name;
//权限类型,菜单/按钮
@Column(columnDefinition = "enum('menu','button')")
private String resourceType;
private String url;
//权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
private String permission;
private Integer parentId;
private String parentIds;
private Boolean available = Boolean.FALSE;
@ManyToMany
@JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "permissionId")}, inverseJoinColumns = {@JoinColumn(name = "roleId")})
private List<SysRole> sysRoles;
//get/set
}
运行应用程序,通过jpa会自动生成表,表名和列名是按照驼峰转下划线的风格
- 用户表: user_info,
- 角色表: sys_role,
- 权限表: sys_permission,
- 用户角色中间表: sys_user_role,
- 角色权限中间表: sys_role_permission
4. 插入测试数据
插入一个admin用户,密码123456
插入三个角色:管理员、VIP会员、test
插入三个权限(资源): 每个资源包含 资源类型(菜单或按钮)、权限标识符(一般是模块:操作这种格式)、url地址
角色-权限:管理员角色中有用户管理、用户添加、用户删除三个权限
用户-角色:admin用户拥有管理员角色,有用户管理、用户添加二个权限,用户删除权限没有
INSERT INTO `user_info` (`uid`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', '管理员', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0);
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (1,0,'管理员','admin');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (2,0,'VIP会员','vip');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (3,1,'test','test');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (1,0,'用户管理',0,'0/','userInfo:view','menu','userInfo/userList');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (2,0,'用户添加',1,'0/1','userInfo:add','button','userInfo/userAdd');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (3,0,'用户删除',1,'0/1','userInfo:del','button','userInfo/userDel');
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (3,2);
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);
5.dao/service
@Service("userInfoService")
public class UserInfoServiceImpl implements UserInfoService {
@Resource
private UserInfoDao userInfoDao;
@Override
public UserInfo findByUserName(String username) {
return userInfoDao.findByUserName(username);
}
}
6.ShiroConfig
Config
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置登录的路径,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
//设置登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
//设置访问没有权限跳转到的界面
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//过滤器链,拦截的顺序是按照配置的顺序来的
//过滤链定义,从上向下顺序执行
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//配置不会被拦截的路径,一般静态资源都不需要拦截,anon代表匿名的不需要拦截的资源,这里的静态资源的匹配模式配置成/static/**,
filterChainDefinitionMap.put("/static/**", "anon");
//登出路径使用logout拦截器
filterChainDefinitionMap.put("/logout", "logout");
//authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
//凭证匹配器
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//散列算法:这里使用MD5算法
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//散列的次数md5(md5(""))
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
@Bean
public AuthorizingRealm authorizingRealm(){
AuthorizingRealm authorizingRealm = new DemoShiroRealm();
authorizingRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return authorizingRealm;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(authorizingRealm());
return securityManager;
}
/**
* 开启shiro aop注解支持
* 使用代理方式;所以需要开启代码支持
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver(){
SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
//数据库异常处理
mappings.setProperty("DatabaseException", "databaseError");
mappings.setProperty("UnauthorizedException","403");
simpleMappingExceptionResolver.setExceptionMappings(mappings);
simpleMappingExceptionResolver.setDefaultErrorView("error");
simpleMappingExceptionResolver.setExceptionAttribute("ex");
return simpleMappingExceptionResolver;
}
}
AuthorizingRealm
public class DemoShiroRealm extends AuthorizingRealm {
@Resource
private UserService userService;
/**
* 授权:SimpleAuthorizationInfo用于存储用户的所有角色(Set<String> roles)和所有权限(Set<String> stringPermissions)信息
* 当执行某个方法时,方法上会有权限注解,例如@RequiresPermissions("userInfo:add"),
* 此时就会去找AuthorizationInfo中的stringPermissions是否包含userInfo:add,如果包含就继续处理,
* 如果不包含则跳转到shiro配置的为授权的地址
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();
for (SysRole role : userInfo.getSysRoles()){
//添加角色
authorizationInfo.addRole(role.getRole());
for (SysPermission permission : role.getSysPermissions()){
//添加权限
authorizationInfo.addStringPermission(permission.getPermission());
}
}
return authorizationInfo;
}
/**
* 认证
* 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确
* 当用户登录时会执行
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
UserInfo userInfo = userService.findByUserName(username);
if(userInfo == null){
return null;
}
//userInfo.getCredentialsSalt()盐的生成方式 这里使用salt=username+salt
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userInfo, userInfo.getPassword(), ByteSource.Util.bytes(userInfo.getCredentialsSalt()), getName());
return authenticationInfo;
}
}
7.controller
HomeController
@Controller
public class HomeController {
@RequestMapping({"/","/index"})
public String index(){
return "index";
}
/**
* 登录时先执行Realm中的认证方法,然后再执行登录方法
* @param request
* @return
*/
@RequestMapping("/login")
public String login(HttpServletRequest request){
//登录失败从request中获取shiro处理的异常信息
//shiroLoginFailure:就是shiro异常类的全类名
String exception = (String) request.getAttribute("shiroLoginFailure");
String msg = "";
if(exception != null){
if(UnknownAccountException.class.getName().equals(exception)){
System.out.println("UnknownAccountException -- > 账号不存在:");
msg = "UnknownAccountException -- > 账号不存在:";
} else if (IncorrectCredentialsException.class.getName().equals(exception)){
System.out.println("IncorrectCredentialsException -- > 密码不正确:");
msg = "IncorrectCredentialsException -- > 密码不正确:";
} else if ("kaptchaValidateFailed".equals(exception)){
System.out.println("kaptchaValidateFailed -- > 验证码错误");
msg = "kaptchaValidateFailed -- > 验证码错误";
}else {
msg = "else >> "+exception;
System.out.println("else -- >" + exception);
}
}
request.setAttribute("msg", msg);
// 此方法不处理登录成功,由shiro进行处理, 应为会在shiro中配置登录成功需要跳转的界面
return "/login";
}
@RequestMapping("/403")
public String unauthorizedRole(){
return "403";
}
}
UserInfoController
@RequestMapping("/userInfo")
@Controller
public class UserInfoController {
/**
* 用户查询
* 查看用户信息的权限
* @return
*/
@RequiresPermissions("userInfo:view")
@RequestMapping("/userList")
public String userInfo(){
return "userInfo";
}
/**
* 用户添加
* 添加用户的权限
* @return
*/
@RequiresPermissions("userInfo:add")
@RequestMapping("/userAdd")
public String userInfoAdd(){
return "userInfoAdd";
}
/**
* 用户删除
* 删除用户的权限
* @return
*/
@RequiresPermissions("userInfo:del")
@RequestMapping("/userDel")
public String userInfoDel(){
return "userInfoDel";
}
}