Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
shiro官网
三个核心组件:Subject, SecurityManager 和 Realms.
org.apache.shiro
shiro-spring-boot-web-starter
1.5.3
存放用户的信息
public class User implements Serializable {
private Integer id;
private String username;
private String password;
/**
* 密码加密用的盐
*/
private String salt;
/**
* 用户拥有的角色
*/
private Set<Role> roles;
//省略... get set方法
}
存放角色的信息
public class Role {
public Role(String roleName, Set<Permission> permissions) {
this.roleName=roleName;
this.permissions=permissions;
}
private Integer id;
private String roleName;
/**
* 角色拥有的权限
*/
private Set<Permission> permissions;
//省略... get set方法
}
存放权限信息
public class Permission {
public Permission(String permissionName){
this.permissionName=permissionName;
}
private Integer id;
private String permissionName;
//省略... get set方法
}
public class CustomRealm extends AuthorizingRealm {
private final Logger logger= LoggerFactory.getLogger(this.getClass());
@Autowired
private UserRepository userRepository;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
User user = (User) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> permissionList = new HashSet<>();
Set<String> roleNameList = new HashSet<>();
for(Role role: user.getRoles()){
for(Permission permission : role.getPermissions()){
//添加权限
permissionList.add(permission.getPermissionName());
}
//添加角色
roleNameList.add(role.getRoleName());
}
info.setStringPermissions(permissionList);
info.setRoles(roleNameList);
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken upt = (UsernamePasswordToken) authenticationToken;
User user=userRepository.findByUserName(upt.getUsername());
if(user==null){
//帐号不存在
logger.info("account does not exist!");
throw new UnknownAccountException();
}
logger.info("account exist ,Certification...!");
//把数据库的密码和(当前登录输入的密码+盐 后加密的密码比对)
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user,user.getPassword(), ByteSource.Util.bytes(user.getSalt()),getName());
return authenticationInfo;
}
}
@Configuration
public class ShiroConfig {
/**
* 配置自定义Realm
*/
@Bean
public CustomRealm userRealm() {
CustomRealm userRealm = new CustomRealm();
//配置使用哈希密码匹配
userRealm.setCredentialsMatcher(credentialsMatcher());
return userRealm;
}
/**
* 设置对应的过滤条件和跳转条件
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
//没有登录的用户请求需要登录的页面时自动跳转到登录页面。
shiroFilterFactoryBean.setLoginUrl("/login");
//filterChainDefinitionMap 配置过滤规则,从上到下的顺序匹配。
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//authc是需要认证后访问的接口,anon是放行的接口
filterChainDefinitionMap.put("/user/**", "authc");
filterChainDefinitionMap.put("/admin/**", "authc");
//除了user和admin路径开头的接口,其它资源都开放
filterChainDefinitionMap.put("/**", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//没有权限默认跳转的页面,登录的用户访问了没有被授权的资源自动跳转到的页面。
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
return shiroFilterFactoryBean;
}
/**
* 设置用于匹配密码的CredentialsMatcher
*/
@Bean
public HashedCredentialsMatcher credentialsMatcher() {
// 散列算法,这里使用更安全的sha256算法
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
// 数据库存储的密码字段使用HEX还是BASE64方式加密
credentialsMatcher.setStoredCredentialsHexEncoded(false);
// 散列迭代次数
credentialsMatcher.setHashIterations(1024);
return credentialsMatcher;
}
/**
* 配置security并设置userReaml,避免xxxx required a bean named 'authorizer' that could not be found.的报错
*/
@Bean
public SessionsSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
return securityManager;
}
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator app=new DefaultAdvisorAutoProxyCreator();
app.setProxyTargetClass(true);
return app;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
}
public class PasswordHelper {
private static RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
//加密算法
private static String algorithmName = Sha256Hash.ALGORITHM_NAME;
//迭代次数
private static int hashIterations = 1024;
//生成随机的盐
public static String randomSalt(){
return randomNumberGenerator.nextBytes().toBase64();
}
public static String encryptPassword(String username, String password,String salt) {
String encryPassword= new SimpleHash(algorithmName, password,
ByteSource.Util.bytes(salt), hashIterations).toBase64();
System.out.printf("username=%s ,password=%s , salt=%s , encryPassword=%s ",username,password,salt,encryPassword);
return encryPassword;
}
public static void main(String[] args) {
String username="admin";
//盐前面必须拼接用户名,不然密码认证的时候会失败 (盐需要存在数据库)
String salt=username+randomSalt();
String newpassword=encryptPassword(username,"123",salt);
}
}
为方便测试,此处制造假数据.实际场景需要重数据库获取
/**
* @author Dominick Li
* @description 初始化用户数据,模拟数据库
**/
@Component
public class UserRepository {
HashMap<String, User> users = new HashMap();
{
//初始化权限
Set<Permission> permissions = new HashSet<>();
permissions.add(new Permission("save"));
permissions.add(new Permission("delete"));
permissions.add(new Permission("select"));
Set<Role> roles = new HashSet<>();
//初始化角色
roles.add(new Role("admin",permissions));
//初始化管理员
User user = new User();
user.setUsername("admin");
String userPassword="123";
//盐前面必须拼接用户名,不然密码认证的时候会失败 (盐需要存在数据库)
String salt=user.getUsername()+PasswordHelper.randomSalt();
user.setPassword(PasswordHelper.encryptPassword(user.getUsername(),userPassword,salt));
user.setSalt(salt);
user.setRoles(roles);
//初始化普通用户信息
Set<Permission> permissions2 = new HashSet<>();
permissions.add(new Permission("select"));
Set<Role> roles2 = new HashSet<>();
roles2.add(new Role("user",permissions2));
User user2 = new User();
user2.setUsername("test");
String user2Password="123";
String salt2=user2.getUsername()+PasswordHelper.randomSalt();
user2.setPassword(PasswordHelper.encryptPassword(user2.getUsername(),user2Password,salt2));
user2.setSalt(salt2);
user2.setRoles(roles2);
users.put("admin",user);
users.put("test",user2);
}
/**
* 模拟重数据库获取用户信息
*/
public User findByUserName(String username) {
return users.get(username);
}
public List<User> findAll() {
List<User> userList = new ArrayList<>();
for (User user : users.values()) {
userList.add(user);
}
return userList;
}
public void deleteByUserName(String username) {
users.remove(username);
}
}
ResponseResult 是我自定义返回数据的map类,大家可以换成自己的返回类型
@PostMapping("/login.do")
@ResponseBody
public ResponseResult login(User user, HttpSession session) {
try {
//进行验证,这里可以捕获异常,然后返回对应信息
UsernamePasswordToken upt = new UsernamePasswordToken(user.getUsername(), user.getPassword());
SecurityUtils.getSubject().login(upt);
} catch (AuthenticationException e) {
e.printStackTrace();
return new ResponseResult(false, "账号或密码错误!");
} catch (AuthorizationException e) {
e.printStackTrace();
return new ResponseResult(false, "没有权限!");
}
return new ResponseResult(true,"登录成功!");
}
@ControllerAdvice
public class GlobalExceptionController {
private final Logger logger= LoggerFactory.getLogger(this.getClass());
private static final String ERROR_MESSAGE = "系统内部错误,请联系管理员!";
@ExceptionHandler(value = Exception.class)
public ModelAndView ExceptionHndler(Exception e) {
ModelAndView mv=new ModelAndView("error");
String msg=ERROR_MESSAGE;
if(e instanceof UnauthorizedException){
msg=e.getMessage();
}
mv.addObject("error",msg);
logger.error("global error:{}", e);
return mv;
}
}
@RequiresRoles( value = {"admin", "user"}, logical = Logical.OR)
@GetMapping("/user/index")
public String user(Model model)
{
List<User> users=userRepository.findAll();
model.addAttribute("userList",users);
return "user/index";
}
@RequiresPermissions(value = {"delete","*"},logical = Logical.OR)
@GetMapping("/user/delete/{username}")
public String delete(@PathVariable String username){
userRepository.deleteByUserName(username);
return "forward:/user/index";
}
教程配套代码已上传到github,https://github.com/Dominick-Li/springboot-master
使用普通用户登录,测试跨权限访问资源
帐号: test 密码: 123
点击导航到管理员页面按钮,跳转管理员页面
因为管理员页面设置了只有拥有admin角色才能访问,test用户只有user角色,所以不能访问,重定向到了错误页
点击删除按钮,删除用户
因为test用户没有delete或者*权限,所有无法执行删除用户操作