GitHub:github.com/baiyuliang/…
一般的,SpringBoot常用的安全模块有Spring下的Security和Apache下的Shiro,Security功能强大但复杂,Shiro则相对小而简单,通常的,Shiro能满足我们实际开发中的绝大部分需求,所以使用Shiro的开发人员也越来越多了!
安全模块的作用:
- 身份认证 (登录验证/加密)
- 授权(授予权限、角色)
- Session管理
- 加密
- 记住我
- ...
所以,安全模块,是一个web网站必不可少的东西了!关于Shiro的详细使用方法,大家可以查看相关文档,本篇属于实战型案例,整合Shiro+加密加盐+角色权限+Session管理+Redis+Shiro标签使用,所以关于更多细节上的东西,还需要大家自行查看文档加以理解!
由于这一篇涉及到了角色和权限,那么在上一篇已经提到过的这两个表要必须创建了,再贴一下:
Role表:
Permission表:
用户表:
注意用户表,前面我们用到的密码,都是明文方式,这肯定是不可取的,这里我改回了加密方式并且是加盐(salt)加密,后面会讲到,数据大家可以自行添加,对应的Role和Permission的javabean不要忘了添加!
首先分析登录认证流程:
- 用户属于账号、密码登录;
- Controller接收到参数后进入Shiro认证流程;
- Shiro验证用户名和密码,通过后保存登录信息并进入授权流程,不通过返回错误信息;
- 授权时,需要根据登录信息,获取该用户的角色,以及该角色对应的权限,并通过Shiro绑定;
根据上面的分析,我们需要先创建Role和Permission表对应的Dao和Service:
RoleRepository:
package com.byl.springbootdemo.dao;
import com.byl.springbootdemo.bean.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RoleRepository extends JpaRepository<Role, Integer> {
}
RoleService:
package com.byl.springbootdemo.service;
import com.byl.springbootdemo.bean.Role;
public interface RoleService {
Role getRoleById(Integer id);
}
RoleServiceImpl:
package com.byl.springboottest.service;
import com.byl.springboottest.bean.Role;
import com.byl.springboottest.dao.RoleRepository;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class RoleServiceImpl implements RoleService {
@Resource
RoleRepository roleRepository;
@Override
public Role getRoleById(Integer id) {
return roleRepository.findById(id).get();
}
}
PermissionRepository:
package com.byl.springbootdemo.dao;
import com.byl.springbootdemo.bean.Permission;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PermissionRepository extends JpaRepository<Permission, Integer> {
Permission findByRoleId(Integer role_id);
}
PermissionService:
package com.byl.springbootdemo.service;
import com.byl.springbootdemo.bean.Permission;
public interface PermissionService {
Permission getPermissionByRoleId(Integer role_id);
}
PermissionServiceImpl:
package com.byl.springboottest.service;
import com.byl.springboottest.bean.Permission;
import com.byl.springboottest.dao.PermissionRepository;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class PermissionServiceImpl implements PermissionService {
@Resource
PermissionRepository permissionRepository;
@Override
public Permission getPermissionByRoleId(Integer role_id) {
return permissionRepository.findByRoleId(role_id);
}
}
pom.xml引入:
<!--权限认证框架shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.6.0</version>
</dependency>
<!-- shiro整合redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.3.1</version>
</dependency>
<!-- thymeleaf整合shiro标签 -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
application添加属性:
# Session超时时间(默认30分钟)
shiro.session.expireTime=30
shiro.jessionid=byl.sessionId
注意,我们使用了Shiro,那么上一篇所使用的的拦截器就不用再使用了,因为拦截器所实现的功能,Shiro已经提供了,大家可以自行注释掉!
创建PermissionRealm:
package com.byl.springboottest.shiro;
import com.byl.springboottest.bean.Permission;
import com.byl.springboottest.bean.Role;
import com.byl.springboottest.bean.User;
import com.byl.springboottest.service.PermissionService;
import com.byl.springboottest.service.RoleService;
import com.byl.springboottest.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
public class PermissionRealm extends AuthorizingRealm {
@Resource
UserService userService;
@Resource
RoleService roleService;
@Resource
PermissionService permissionService;
/**
* 授权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("进入授权>>");
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
User user = (User) SecurityUtils.getSubject().getPrincipal();
Role role = roleService.getRoleById(user.getRoleId());
System.out.println("角色>>"+role.getName());
simpleAuthorizationInfo.addRole(role.getName());//角色:superadmin,admin,user
Permission permission = permissionService.getPermissionByRoleId(role.getId());
System.out.println("权限>>"+permission.getName());
simpleAuthorizationInfo.addStringPermission(permission.getName());//添加权限
return simpleAuthorizationInfo;
}
/**
* 认证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
return null;
}
String username = (String) authenticationToken.getPrincipal();
//获取用户信息
User user = userService.getUserByName(username);
if (user == null) {
return null;
} else {
ByteSource salt = ByteSource.Util.bytes(user.getUsername() + user.getSalt());//参数要与加密时方式一致(用户名+盐值)
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName());
clearCachedAuthorizationInfo();
return simpleAuthenticationInfo;
}
}
/**
* 清理缓存权限
*/
public void clearCachedAuthorizationInfo() {
this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
}
}
重写AuthorizingRealm 的两个方法:doGetAuthorizationInfo 授权和doGetAuthorizationInfo认证,由于使用了加盐方式,我们需要一个Util类,来处理盐值的生成与密码的加密逻辑:
SaltUtil:
package com.byl.springbootdemo.utils;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
public class SaltUtil {
public static String HASHALGORITHMNAME = "md5";//加密方式
public static int HASHITERATIONS = 1024;//加密次数
public static String randomSalt() {
// 一个Byte占两个字节,此处生成的3字节,字符串长度为6
SecureRandomNumberGenerator secureRandom = new SecureRandomNumberGenerator();
String hex = secureRandom.nextBytes(3).toHex();
return hex;
}
public static String encryptPassword(String username, String password) {
String salt= randomSalt();
String newPassword = new SimpleHash(HASHALGORITHMNAME, password,
ByteSource.Util.bytes(username +salt),
HASHITERATIONS
).toHex();
System.out.println("salt>>"+salt);
System.out.println("newPassword>>"+newPassword);
return newPassword;
}
}
什么是加盐? 就是撒盐哥往牛排上撒点盐?非也!加盐其实是对普通的md5等加密方式做了进一步处理,目的就是防止暴力破解,基本上无解!我们知道,普通的加密方式,就是对明文密码进行1-2次的md5加密,虽然不可逆,但为了尽可能的安全(防止被密码库暴力破解),此时就可以在原来明文密码的基础上,加一些“杂质”混合进去,而且这些杂质还是动态的,那么就算大罗金仙来了,也破解不了了!如上面的代码,密码生成方式为:
- 先生成随机盐值,每个用户的盐值都不相同;
- 加密时,将用户名+盐值,混合用户自己输入的密码加密,得到最终密码;
- 记录生成的盐值和最终密码,存入数据库;
在登录验证时,就可以通过数据库中保存的盐值和密码,与用户输入的账号和密码进行验证了:
ByteSource salt = ByteSource.Util.bytes(user.getUsername() + user.getSalt());//参数要与加密时方式一致(用户名+盐值)
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName())
创建ShiroConfig:
package com.byl.springboottest.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.byl.springboottest.shiro.PermissionRealm;
import com.byl.springboottest.shiro.RolesAuthorizationFilter;
import com.byl.springboottest.utils.SaltUtil;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.timeout}")
private int redisTimeout;
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.password}")
private String redisPassword;
@Value("${shiro.session.expireTime}")
private int expireTime;
@Value("${shiro.jessionid}")
private String jessionId;
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
//对所有用户认证
map.put("/**", "authc");
//允许访问
map.put("/css/**", "anon");
map.put("/images/**", "anon");
map.put("/js/**", "anon");
map.put("/lib/**", "anon");
map.put("/login.html", "anon");
map.put("/user/login", "anon");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error/403.html");
//页面角色权限控制
map.put("/level1/**", "anyRoleFilter[user,admin,superadmin]");
map.put("/level2/**", "anyRoleFilter[admin,superadmin]");
map.put("/level3/**", "anyRoleFilter[superadmin]");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("anyRoleFilter", new RolesAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filterMap);
return shiroFilterFactoryBean;
}
/**
* 自定义密码校验器
*
* @return
*/
@Bean
public CredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName(SaltUtil.HASHALGORITHMNAME);
credentialsMatcher.setHashIterations(SaltUtil.HASHITERATIONS);
return credentialsMatcher;
}
//将自己的验证方式加入容器
@Bean
public PermissionRealm permissionRealm(CredentialsMatcher credentialsMatcher) {
PermissionRealm customRealm = new PermissionRealm();
customRealm.setCredentialsMatcher(credentialsMatcher);
return customRealm;
}
/**
* 配置SecurityManager
*
* @return
*/
@Bean
public SecurityManager securityManager(CredentialsMatcher credentialsMatcher) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(permissionRealm(credentialsMatcher)); // 设置realm
securityManager.setSessionManager(sessionManager()); // 设置sessionManager
securityManager.setCacheManager(myRedisCacheManager()); // 设置cacheManager
return securityManager;
}
/**
* redisCacheManager 缓存 redis实现
* shiro-redis
* We need a field to identify this Cache Object in Redis. So you need to defined an id field which you can get unique id to identify this principal.
* For example, if you use UserInfo as Principal class, the id field maybe userId, userName, email, etc. For example, getUserId(), getUserName(), getEmail(), etc.
* Default value is "id", that means your principal object has a method called "getId()"
*
* @return
*/
@Bean
public RedisCacheManager myRedisCacheManager() {
RedisCacheManager cacheManager = new RedisCacheManager();
cacheManager.setRedisManager(redisManager());
cacheManager.setPrincipalIdFieldName("username");//主键名称(默认id)
return cacheManager;
}
/**
* 配置shiro redisManager
* shiro-redis
*
* @return
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisHost + ":" + redisPort);
redisManager.setTimeout(redisTimeout);
redisManager.setPassword(redisPassword);
return redisManager;
}
/**
* SessionManager
* shiro-redis
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setGlobalSessionTimeout(expireTime * 60 * 1000);
sessionManager.setSessionIdUrlRewritingEnabled(false);//禁用url重写,否则浏览器中会在url后面自动加上:xx/login;JSESSIONID=xxx
sessionManager.setSessionIdCookie(getSessionIdCookie());
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* 给shiro的sessionId默认的JSSESSIONID名字改掉
*
* @return
*/
@Bean
public SimpleCookie getSessionIdCookie() {
SimpleCookie simpleCookie = new SimpleCookie(jessionId);
return simpleCookie;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* shiro-redis
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO sessionDAO = new RedisSessionDAO();
sessionDAO.setRedisManager(redisManager());
sessionDAO.setSessionIdGenerator(sessionIdGenerator());
return sessionDAO;
}
/**
* Session ID 生成器
*
* @return
*/
@Bean
public JavaUuidSessionIdGenerator sessionIdGenerator() {
return new JavaUuidSessionIdGenerator();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/***
* 使授权注解起作用
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
//配置ShiroDialect:用于thymeleaf和shiro标签配合使用
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
}
RolesAuthorizationFilter:
package com.byl.springbootdemo.shiro;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.util.List;
/**
* 角色授权过滤器
*/
public class RolesAuthorizationFilter extends AuthorizationFilter {
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
String[] rolesArray = (String[]) mappedValue;
if (rolesArray == null || rolesArray.length == 0) {
return false;
}
List<String> roles = CollectionUtils.asList(rolesArray);
boolean[] hasRoles = subject.hasRoles(roles);
for (boolean hasRole : hasRoles) {
if (hasRole) {
return true;
}
}
return false;
}
}
我将资源文件做了一些更改:
大家可以在这里下载源码,以供参考:download.csdn.net/download/ba…
UserController里的登录逻辑,我们需要重新来写了:
@PostMapping("/login")
public ResponseData login(@RequestParam Map<String, String> params) {
logger.error(params.toString());
if (params.get("username") == null || params.get("password") == null) return new ResponseData(-1, "账号或账号不能为空");
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(params.get("username"), params.get("password"));
// usernamePasswordToken.setRememberMe(true);//记住密码
try {
subject.login(usernamePasswordToken);
subject.hasRole("admin");
} catch (UnknownAccountException e) {
return new ResponseData(-1, "用户不存在");
} catch (AuthenticationException e) {
return new ResponseData(-1, "账号或密码错误");
} catch (AuthorizationException e) {
return new ResponseData(-1, "没有权限");
}
return new ResponseData(1, "登录成功");
}
关于参数常用的接收方式有以下几种:
- JavaBean:比如,login(@RequestBody User user){},可以直接取对象里面的字段,但记住一定要有@RequestBody注解(提交的参数要与字段对应);
- Map:如上面的代码,要有@RequestParam注解(提交的参数要与key对应);
- 具体参数:如login(String username,String password){}(参数注解可以省略,提交的参数要与方法参数对应);
此时,我们就可以来做登录验证了,我们可以通过网页注册的形式,将加密过的密码存入数据库,但我并没有实现,这个实现就交给你们了!我是使用test方法,直接生成salt和密码,并存入了数据库,大家测试阶段也可以这么做,能省不少时间!
@Test
void testPwdSalt(){
SaltUtil.encryptPassword("admin","admin");
}
执行结果:
salt>>d1af77
newPassword>>c4b33995b676a712c5b48a3c4fa38e85
记录这两个值,存入账号为admin的数据库,可以自己生成!
浏览器进入登录页http://localhost:8080/byl/login,开始验证:
登录成功,查看控制台:
授权成功!
注意Controller中的:
subject.login(usernamePasswordToken);
subject.hasRole("admin");
这两句代码,第一个就是登录验证,第二个就是授权!此时打开redis管理器:
证明,和redis已经整合成功!