Springboot2.0 集成shiro权限管理
在springboot中结合shiro搭建权限管理,其中几个小细节的地方对新手不友好,搭建过程容易遇坑,记录一下。关键的地方也给注释了。
版本:springboot版本2.x,shiro1.4
一、依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
二、实体
实体有三个,根据规则会自动生成两个中间表,数据库实际上会生成5张表。
分别是User,SysRole,SysPermission,中间表按照@JoinTable来生成。
@Entity public class User { @Id @GenericGenerator(name="generator",strategy = "native") @GeneratedValue(generator = "generator") private Integer userId; @Column(nullable = false, unique = true) private String userName; //登录用户名 @Column(nullable = false) private String name;//名称(昵称或者真实姓名,根据实际情况定义) @Column(nullable = false) private String password; private String salt;//加密密码的盐 private byte state;//用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定. @ManyToMany(fetch= FetchType.EAGER)//立即从数据库中进行加载数据; @JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "userId") }, inverseJoinColumns ={@JoinColumn(name = "roleId") }) private List<SysRole> roleList;// 一个用户具有多个角色 @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime createTime;//创建时间 @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate expiredDate;//过期日期 private String email; public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; } public LocalDate getExpiredDate() { return expiredDate; } public void setExpiredDate(LocalDate expiredDate) { this.expiredDate = expiredDate; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getSalt() { return salt; } public void setSalt(String salt) { this.salt = salt; } public byte getState() { return state; } public void setState(byte state) { this.state = state; } public List<SysRole> getRoleList() { return roleList; } public void setRoleList(List<SysRole> roleList) { this.roleList = roleList; } /** * 密码盐. 重新对盐重新进行了定义,用户名+salt,这样就不容易被破解,可以采用多种方式定义加盐 * @return */ public String getCredentialsSalt(){ return this.userName+this.salt; } }
@Entity public class SysRole { @Id @GenericGenerator(name="generator",strategy = "native") @GeneratedValue(generator = "generator") private Integer roleId; // 编号 @Column(nullable = false, unique = true) private String role; // 角色标识程序中判断使用,如"admin",这个是唯一的: private String description; // 角色描述,UI界面显示使用 private Boolean available = Boolean.TRUE; // 是否可用,如果不可用将不会添加给用户 //角色 -- 权限关系:多对多关系; @ManyToMany(fetch= FetchType.EAGER) @JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")}) private List<SysPermission> permissions; // 用户 - 角色关系定义; @ManyToMany @JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="userId")}) private List<User> users;// 一个角色对应多个用户 public Integer getRoleId() { return roleId; } public void setRoleId(Integer roleId) { this.roleId = roleId; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Boolean getAvailable() { return available; } public void setAvailable(Boolean available) { this.available = available; } public List<SysPermission> getPermissions() { return permissions; } public void setPermissions(List<SysPermission> permissions) { this.permissions = permissions; } public List<User> getUsers() { return users; } public void setUsers(List<User> users) { this.users = users; } }
@Entity public class SysPermission { @Id @GenericGenerator(name="generator",strategy = "native") @GeneratedValue(generator = "generator") private Integer permissionId;//主键. @Column(nullable = false) private String permissionName;//名称. @Column(columnDefinition="enum('menu','button')") private String resourceType;//资源类型,[menu|button] private String url;//资源路径. private String permission; //权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view private Long parentId; //父编号 private String parentIds; //父编号列表 private Boolean available = Boolean.TRUE; //角色 -- 权限关系:多对多关系; @ManyToMany @JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")}) private List<SysRole> roles; public Integer getPermissionId() { return permissionId; } public void setPermissionId(Integer permissionId) { this.permissionId = permissionId; } public String getPermissionName() { return permissionName; } public void setPermissionName(String permissionName) { this.permissionName = permissionName; } public String getResourceType() { return resourceType; } public void setResourceType(String resourceType) { this.resourceType = resourceType; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getPermission() { return permission; } public void setPermission(String permission) { this.permission = permission; } public Long getParentId() { return parentId; } public void setParentId(Long parentId) { this.parentId = parentId; } public String getParentIds() { return parentIds; } public void setParentIds(String parentIds) { this.parentIds = parentIds; } public Boolean getAvailable() { return available; } public void setAvailable(Boolean available) { this.available = available; } public List<SysRole> getRoles() { return roles; } public void setRoles(List<SysRole> roles) { this.roles = roles; } }
三、DAO,这里用JPA
public interface UserRepository extends JpaRepository<User,Integer> { User findByUserName(String userName); }
依赖是
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
四、Service
public interface UserService { User findByUserName(String userName); } public interface LoginService { LoginResult login(String userName,String password); void logout(); }
五、Service.impl
@Service public class UserServiceImpl implements UserService { @Resource private UserRepository userRepository; @Override public User findByUserName(String userName) { return userRepository.findByUserName(userName); } }
//内部使用的一个model,根据需要扩展
public class LoginResult { private boolean isLogin = false; private String result; public boolean isLogin() { return isLogin; } public void setLogin(boolean login) { isLogin = login; } public String getResult() { return result; } public void setResult(String result) { this.result = result; } }
@Service public class LoginServiceImpl implements LoginService { @Override public LoginResult login(String userName, String password) { LoginResult loginResult = new LoginResult(); if(userName==null || userName.isEmpty()) { loginResult.setLogin(false); loginResult.setResult("用户名为空"); return loginResult; } String msg=""; // 1、获取Subject实例对象 Subject currentUser = SecurityUtils.getSubject(); // // 2、判断当前用户是否登录 // if (currentUser.isAuthenticated() == false) { // // } // 3、将用户名和密码封装到UsernamePasswordToken UsernamePasswordToken token = new UsernamePasswordToken(userName, password); // 4、认证 try { currentUser.login(token);// 传到MyAuthorizingRealm类中的方法进行认证 Session session = currentUser.getSession(); session.setAttribute("userName", userName); loginResult.setLogin(true); return loginResult; //return "/index"; }catch (UnknownAccountException e) { e.printStackTrace(); msg = "UnknownAccountException -- > 账号不存在:"; } catch (IncorrectCredentialsException e) { msg = "IncorrectCredentialsException -- > 密码不正确:"; } catch (AuthenticationException e) { e.printStackTrace(); msg="用户验证失败"; } loginResult.setLogin(false); loginResult.setResult(msg); return loginResult; } @Override public void logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); } }
六、config,配置类
shiro核心配置
public class MyShiroRealm extends AuthorizingRealm { @Resource private UserService userService; //权限信息,包括角色以及权限 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()"); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //如果身份认证的时候没有传入User对象,这里只能取到userName //也就是SimpleAuthenticationInfo构造的时候第一个参数传递需要User对象 User user = (User)principals.getPrimaryPrincipal(); for(SysRole role:user.getRoleList()){ authorizationInfo.addRole(role.getRole()); for(SysPermission p:role.getPermissions()){ authorizationInfo.addStringPermission(p.getPermission()); } } return authorizationInfo; } /*主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。*/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("MyShiroRealm.doGetAuthenticationInfo()"); //获取用户的输入的账号. String userName = (String)token.getPrincipal(); System.out.println(token.getCredentials()); //通过username从数据库中查找 User对象. //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法 User user = userService.findByUserName(userName); System.out.println("----->>user="+user); if(user == null){ return null; } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, //这里传入的是user对象,比对的是用户名,直接传入用户名也没错,但是在授权部分就需要自己重新从数据库里取权限 user.getPassword(), //密码 ByteSource.Util.bytes(user.getCredentialsSalt()),//salt=username+salt getName() //realm name ); return authenticationInfo; } }
@Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { System.out.println("ShiroConfiguration.shirFilter()"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //拦截器. Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>(); // 配置不会被拦截的链接 顺序判断,因为前端模板采用了thymeleaf,这里不能直接使用 ("/static/**", "anon")来配置匿名访问,必须配置到每个静态目录 filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/fonts/**", "anon"); filterChainDefinitionMap.put("/img/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/html/**", "anon"); //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了 filterChainDefinitionMap.put("/logout", "logout"); //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了; //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问--> filterChainDefinitionMap.put("/**", "authc"); // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl("/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/index"); //未授权界面; shiroFilterFactoryBean.setUnauthorizedUrl("/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 凭证匹配器 * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 * ) * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法; hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5("")); return hashedCredentialsMatcher; } @Bean public MyShiroRealm myShiroRealm(){ MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } @Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; } /** * 开启shiro aop注解支持. * 使用代理方式;所以需要开启代码支持; * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } @Bean(name="simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver(); Properties mappings = new Properties(); mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理 mappings.setProperty("UnauthorizedException","/user/403"); r.setExceptionMappings(mappings); // None by default r.setDefaultErrorView("error"); // No default r.setExceptionAttribute("exception"); // Default is "exception" //r.setWarnLogCategory("example.MvcLogger"); // No default return r; } }
七、controller
HomeController用来处理登录
@Controller public class HomeController { @Resource private LoginService loginService; @RequestMapping({"/","/index"}) public String index(){ return"/index"; } @RequestMapping("/403") public String unauthorizedRole(){ System.out.println("------没有权限-------"); return "/user/403"; } @RequestMapping(value = "/login",method = RequestMethod.GET) public String toLogin(Map<String, Object> map,HttpServletRequest request) { loginService.logout(); return "/user/login"; } @RequestMapping(value = "/login",method = RequestMethod.POST) public String login(Map<String, Object> map,HttpServletRequest request) throws Exception{ System.out.println("login()"); String userName = request.getParameter("userName"); String password = request.getParameter("password"); LoginResult loginResult = loginService.login(userName,password); if(loginResult.isLogin()) { return "/index"; } else { map.put("msg",loginResult.getResult()); map.put("userName",userName); return "/user/login"; } } @RequestMapping("/logout") public String logOut(HttpSession session) { loginService.logout(); return "/user/login"; } }
UserController用来测试访问,权限全部采用注解的方式。
@Controller @RequestMapping("/user") public class UserController { /** * 用户查询. * @return */ @RequestMapping("/userList") @RequiresPermissions("user:view")//权限管理; public String userInfo(){ return "userList"; } /** * 用户添加; * @return */ @RequestMapping("/userAdd") @RequiresPermissions("user:add")//权限管理; public String userInfoAdd(){ return "userAdd"; } /** * 用户删除; * @return */ @RequestMapping("/userDel") @RequiresPermissions("user:del")//权限管理; public String userDel(){ return "userDel"; } }
八、html
Login.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="../static/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}"></link> <title>用户登录</title> <style> .bgColor{ background-color:rgba(243,66,111,0.15) } .divBorder{ border: solid 1px rgba(12,24,255,0.15); padding: 10px; margin-top: 10px; border-radius: 10px; text-align: center; vertical-align: middle; } .h4font{ margin-top: 0px; font-family: 微软雅黑; font-weight: 500; } .center { padding: 20% 0; } </style> </head> <body> <div class="container"> <div class="row center"> <div class="divBorder col-sm-offset-3 col-sm-6"> <h3 class="panel panel-heading h4font"> 用户登录 </h3> <h4 th:text="${msg}"></h4> <form class="form-horizontal" th:action="@{/login}" method="post"> <div class="input-group input-group-lg"> <span class="input-group-addon"><i class="glyphicon glyphicon-user" aria-hidden="true"></i></span> <input type="text" class="form-control" id="userName" name="userName" placeholder="请输入用户名称" th:value="${userName}"/> </div> <br> <div class="input-group input-group-lg"> <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i></span> <input type="password" class="form-control" id="password" name="password" placeholder="请输入密码"/> </div> <br> <input type="submit" class="btn btn-lg btn-block btn-info" value="登 录"> </form> </div> </div> </div> </body> </html>
九、数据库里预设一些数据
注意admin的密码是123456,这里保存的是加密后的密码,根据前面的设置,是md5,散列2次。
登录的时候shiro会根据配置自动给密码123456加密,然后与数据库里取出的密码比对。
注意先运行一遍程序,JPA生成数据库表后,手工执行sql脚本插入样本数据。
INSERT INTO `user` (`userId`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', '管理员', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0); INSERT INTO `syspermission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) VALUES (1,0,'用户管理',0,'0/','user:view','menu','user/userList'); INSERT INTO `syspermission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) VALUES (2,0,'用户添加',1,'0/1','user:add','button','user/userAdd'); INSERT INTO `syspermission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) VALUES (3,0,'用户删除',1,'0/1','user:del','button','user/userDel'); INSERT INTO `sysrole` (`roleid`,`available`,`description`,`role`) VALUES (1,0,'管理员','admin'); INSERT INTO `sysrole` (`roleid`,`available`,`description`,`role`) VALUES (2,0,'VIP会员','vip'); INSERT INTO `sysrole` (`roleid`,`available`,`description`,`role`) VALUES (3,1,'test','test'); INSERT INTO `sysrolepermission` (`permissionid`,`roleid`) VALUES (1,1); INSERT INTO `sysrolepermission` (`permissionid`,`roleid`) VALUES (2,1); INSERT INTO `sysrolepermission` (`permissionid`,`roleid`) VALUES (3,2); INSERT INTO `sysuserrole` (`roleid`,`userId`) VALUES (1,1);
十、其他配置
Application.yml
server: servlet: context-path: / port: 80 spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/crmData?characterEncoding=utf8&useSSL=false username: root password: root jpa: hibernate: ddl-auto: update naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl #按字段名字建表 show-sql: true database: mysql database-platform: org.hibernate.dialect.MySQL5InnoDBDialect thymeleaf: cache: false messages: basename: myconfig
所有依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
<!--<version>3.0.1.RELEASE</version>-->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
目录图:
其他的html页面自己随便生成就可以。
到此基本实现简单的权限管理。
代码可以参考这个地址:https://github.com/asker124143222/bus