在springboot中结合shiro搭建权限管理,其中几个小细节的地方对新手不友好,搭建过程容易遇坑,记录一下。关键的地方也给注释了。
版本:springboot版本2.x,shiro1.4
一、依赖
<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
@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
} public void setRoleList(List
} /** * 密码盐. 重新对盐重新进行了定义,用户名+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
@ManyToMany
@JoinTable(name=“SysUserRole”,joinColumns={@JoinColumn(name=”roleId”)},inverseJoinColumns={@JoinColumn(name=”userId”)}) private List
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
} 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
} public void setRoles(List
}
}
三、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”); //:这是一个坑呢,一不小心代码就不好使了; //
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
<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>