使用springboot整合shiro 实现登录、权限控制。
1、用户类、角色类、权限类
@Data
@AllArgsConstructor
public class User {
private String token;
private String username;
private String password;
/**
* 用户对应的角色集合
*/
private Set<Role> roles;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public User() {
}
}
@Data
@AllArgsConstructor
public class Role {
private String id;
private String roleName;
/**
* 角色对应权限集合
*/
private Set<Permissions> permissions;
}
@Data
@AllArgsConstructor
public class Permissions {
private String id;
private String permissionsName;
}
2、maven文件
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>${shiro-spring.version}version>
dependency>
3、realm文件,配置登录验证、权限逻辑
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 授权逻辑
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权逻辑");
//获取登录用户名
Object primaryPrincipal = principalCollection.getPrimaryPrincipal();
//查询用户名称
User user = (User) primaryPrincipal;
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (Role role : user.getRoles()) {
//添加角色
simpleAuthorizationInfo.addRole(role.getRoleName());
//添加权限
for (Permissions permissions : role.getPermissions()) {
simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
}
}
return simpleAuthorizationInfo;
}
/**
* 认证逻辑
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证逻辑");
// 编写shiro判断逻辑,判断用户名和密码
System.out.println(authenticationToken.toString());
// 1. 判断用户名
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
User byUsername = userService.getByUsername(username);
if (byUsername == null) {
//用户名不存在
return null;//shiro底层会抛出UnknownAccountException
}
// 2. 判断密码
// 参数1:需要返回给login方法的数据;参数2:数据库密码,shiro会自动判断
return new SimpleAuthenticationInfo(byUsername, byUsername.getPassword(), "");
}
}
4、编写shiro配置类
@Configuration
public class ShiroConfig {
//Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
//登出
map.put("/logout", "logout");
//对所有用户认证
map.put("/**", "authc");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
// //首页
// shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 权限管理,配置主要是Realm的管理认证
* 创建DefaultWebSecurityManager
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
return securityManager;
}
/**
* 创建Realm
*/
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
/**
* 开启Shiro注解(如@RequiresRoles,@RequiresPermissions),
* 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
5、编写userService模拟数据库查询
@Service
public class UserService {
//模拟从数据库中查询数据
private volatile static Map<String, User> users = new ConcurrentHashMap<>();
static {
Permissions permissions1 = new Permissions("1", "select");
Permissions permissions2 = new Permissions("2", "add");
//拥有所有权限
Set<Permissions> permissionsSet = new HashSet<>();
permissionsSet.add(permissions1);
permissionsSet.add(permissions2);
Role role = new Role("1", "admin", permissionsSet);
Set<Role> roleSet = new HashSet<>();
roleSet.add(role);
User user = new User("1", "kaico", "123456", roleSet);
users.put(user.getUsername(), user);
//至于查询的权限
Set<Permissions> permissionsSet1 = new HashSet<>();
permissionsSet1.add(permissions1);
Role role1 = new Role("2", "user", permissionsSet1);
Set<Role> roleSet1 = new HashSet<>();
roleSet1.add(role1);
User user1 = new User("2", "jing", "123456", roleSet1);
users.put(user1.getUsername(), user1);
}
public User getByUsername(String username){
return users.get(username);
}
}
6、编写全局异常捕获类,用于处理没有权限抛的异常
@ControllerAdvice
@Slf4j
public class MyExceptionHandler {
@ExceptionHandler
@ResponseBody
public String ErrorHandler(AuthorizationException e) {
log.error("没有通过权限验证!", e);
return "没有通过权限验证!";
}
}
7、编写测试类
@RestController
@Log4j2
public class TestController {
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login(String username, String password){
System.out.println("login");
//使用shiro编写认证操作
//获取Subject
Subject subject = SecurityUtils.getSubject();
//封装用户数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
//进行验证,这里可以捕获异常,然后返回对应信息
subject.login(token);
} catch (UnknownAccountException e) {
log.error("用户名不存在!", e);
return "用户名不存在!";
} catch (AuthenticationException e) {
log.error("账号或密码错误!", e);
return "账号或密码错误!";
} catch (AuthorizationException e) {
log.error("没有权限!", e);
return "没有权限";
}
return "login success";
}
@RequestMapping(value = "/add", method = RequestMethod.GET)
@RequiresPermissions(value = "add")
public String add(){
System.out.println("add");
return "add";
}
@RequestMapping(value = "/select", method = RequestMethod.GET)
@RequiresPermissions(value = "select")
public String select(){
System.out.println("select");
return "select";
}
@RequestMapping(value = "/loginOut", method = RequestMethod.GET)
public String loginOut(){
//获取Subject
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "loginOut";
}
@RequestMapping(value = "/error", method = RequestMethod.GET)
public String error(){
return "error";
}
}
数据库中保存的密码都是明文的,一旦数据库数据泄露,那就会造成不可估算的损失,所以我们通常都会使用非对称加密,简单理解也就是不可逆的加密,而 md5 加密算法就是符合这样的一种算法。
既然相同的密码 md5 一样,那么我们就让我们的原始密码再加一个随机数,然后再进行 md5 加密,这个随机数就是我们说的盐(salt),这样处理下来就能得到不同的 Md5 值,当然我们需要把这个随机数盐也保存进数据库中,以便我们进行验证。
1、修改realm的验证配置:主要修改返回的 SimpleAuthenticationInfo 对象增加盐参数。
/**
* 认证逻辑
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证逻辑");
// 编写shiro判断逻辑,判断用户名和密码
System.out.println(authenticationToken.toString());
// 1. 判断用户名
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
User byUsername = userService.getByUsername(username);
if (byUsername == null) {
//用户名不存在
return null;//shiro底层会抛出UnknownAccountException
}
// 2. 判断密码
// 参数1:需要返回给login方法的数据;参数2:数据库密码,shiro会自动判断 参数3:密码加密的盐
return new SimpleAuthenticationInfo(byUsername, byUsername.getPassword(), ByteSource.Util.bytes(username), "");
}
2、修改shiro配置文件:给userRealm bean 设置加密算法
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
// 设置加密算法
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher("md5");
// 设置加密次数
credentialsMatcher.setHashIterations(1);
userRealm.setCredentialsMatcher(credentialsMatcher);
return userRealm;
}
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
与web中的 HttpServletRequest.getSession(boolean create) 类似!
Subject.getSession(true)。即如果当前没有创建session对象会创建一个;
Subject.getSession(false),如果当前没有创建session对象则返回null。
注意:session存入redis中,用户类User需要序列化。
1、增加redis依赖,增加redis配置信息,shiro配置信息
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
spring:
redis:
host: www.kaicostudy.com
port: 6379
database: 0
shiro:
jessionid: kaico
loginUrl: /login
session:
expireTime: 43200000
2、增加session dao层操作,实线将session存储到redis中
@Service
@Log4j2
public class RedisSessionDao extends AbstractSessionDAO {
// Session超时时间,单位为毫秒
@Value("${shiro.session.expireTime}")
private Long expireTime;
@Autowired
private RedisTemplate redisTemplate;// Redis操作类,对这个使用不熟悉的,可以参考前面的博客
public RedisSessionDao() {
super();
}
public RedisSessionDao(long expireTime, RedisTemplate redisTemplate) {
super();
this.expireTime = expireTime;
this.redisTemplate = redisTemplate;
}
@Override // 更新session
public void update(Session session) throws UnknownSessionException {
log.info("===============update================");
if (session == null || session.getId() == null) {
return;
}
session.setTimeout(expireTime);
redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.MILLISECONDS);
}
@Override // 删除session
public void delete(Session session) {
log.info("===============delete================");
if (null == session) {
return;
}
redisTemplate.opsForValue().getOperations().delete(session.getId());
}
@Override
// 获取活跃的session,可以用来统计在线人数,如果要实现这个功能,可以在将session加入redis时指定一个session前缀,统计的时候则使用keys("session-prefix*")的方式来模糊查找redis中所有的session集合
public Collection<Session> getActiveSessions() {
log.info("==============getActiveSessions=================");
return redisTemplate.keys("*");
}
@Override// 加入session
protected Serializable doCreate(Session session) {
log.info("===============doCreate================");
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.MILLISECONDS);
return sessionId;
}
@Override// 读取session
protected Session doReadSession(Serializable sessionId) {
log.info("==============doReadSession=================");
if (sessionId == null) {
return null;
}
return (Session) redisTemplate.opsForValue().get(sessionId);
}
public long getExpireTime() {
return expireTime;
}
public void setExpireTime(long expireTime) {
this.expireTime = expireTime;
}
public RedisTemplate getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
3、将重写的RedisSessionDao 接入到shiro 中的sessionManager,shiro配置类代码
@Log4j2
@Configuration
public class ShiroConfig {
@Value("${shiro.loginUrl}")
private String loginUrl;
@Value("${shiro.jessionid}")
private String jessionId;
@Value("${shiro.session.expireTime}")
private Long expireTime;
//Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
//登出
map.put("/logout", "logout");
//对所有用户认证
map.put("/**", "authc");
//登录
log.info("loginUrl:" + loginUrl);
shiroFilterFactoryBean.setLoginUrl(loginUrl);
// //首页
// shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 权限管理,配置主要是Realm的管理认证
* 创建DefaultWebSecurityManager
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
//将sessionManager 注入到securityManager
securityManager.setSessionManager(defaultWebSessionManager());
return securityManager;
}
/**
* 创建Realm
*/
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
// 设置加密算法
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher("md5");
// 设置加密次数
credentialsMatcher.setHashIterations(1);
userRealm.setCredentialsMatcher(credentialsMatcher);
return userRealm;
}
/**
* 开启Shiro注解(如@RequiresRoles,@RequiresPermissions),
* 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 给shiro的sessionId默认的JSSESSIONID名字改掉
* @return
*/
@Bean(name="sessionIdCookie")
public SimpleCookie getSessionIdCookie(){
SimpleCookie simpleCookie = new SimpleCookie(jessionId);
return simpleCookie;
}
@Bean
public RedisSessionDao getRedisSessionDao(){
return new RedisSessionDao();
}
/**
* @see DefaultWebSessionManager
* @return
*/
@Bean(name="sessionManager")
public DefaultWebSessionManager defaultWebSessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//sessionManager.setCacheManager(cacheManager());
sessionManager.setGlobalSessionTimeout(expireTime);
sessionManager.setDeleteInvalidSessions(true);
//关键在这里
sessionManager.setSessionDAO(getRedisSessionDao());
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setDeleteInvalidSessions(true);
sessionManager.setSessionIdCookie(getSessionIdCookie());
return sessionManager;
}
}
实现缓存的作用,这样每次请求接口不用走realm权限认证的方法了。提高效率。
工具类
@Component
public class ApplicationContextUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
//获取上下文路径
public static ApplicationContext getContext(){
return context;
}
//获取bean
public static Object getBean(String beanName){
return context.getBean(beanName);
}
}
//解决shiro盐加密的序列化问题
public class ByteSourceUtils {
public static ByteSource bytes(byte[] bytes) {
return new SimpleByteSource(bytes);
}
public static ByteSource bytes(String arg0) {
return new SimpleByteSource(arg0.getBytes());
}
}
redis缓存类,实现shiro的 Cache 接口,使用redis实现缓存
@Log4j2
public class RedisCache<K,V> implements Cache<K,V> {
private String name ;
public RedisCache(){
}
public RedisCache(String name){
log.info("name="+name);
this.name = name;
}
private RedisTemplate getRedisTemplate(){
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
@Override
public V get(K k) throws CacheException {
log.info("------------------get from "+k.toString());
return (V) getRedisTemplate().opsForHash().get(name,k.toString());
}
@Override
public V put(K k, V v) throws CacheException {
log.info("------------------put "+ v +" with "+k.toString());
getRedisTemplate().opsForHash().put(name,k.toString(),v);
return null;
}
@Override
public V remove(K k) throws CacheException {
log.info("------------------delete "+k.toString());
getRedisTemplate().opsForHash().delete(name,k.toString());
return null;
}
@Override
public void clear() throws CacheException {
log.info("------------------clear");
getRedisTemplate().opsForHash().delete(name);
}
@Override
public int size() {
return 0;
}
@Override
public Set<K> keys() {
return getRedisTemplate().opsForHash().keys(this.name);
}
@Override
public Collection<V> values() {
return getRedisTemplate().opsForHash().values(this.name);
}
}
RedisCacheManager 管理器
public class RedisCacheManager implements CacheManager {
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
//返回自定义的缓存实现
return new RedisCache<K,V>(s);
}
}
由于shiro中提供的simpleByteSource实现,没有实现序列化,所以在认证时出现错误信息
public class SimpleByteSource extends org.apache.shiro.util.SimpleByteSource implements Serializable {
private static final long serialVersionUID = 5528101080905698238L;
public SimpleByteSource(byte[] bytes) {
super(bytes);
}
}
realm认证时,传入的盐用 MyByteSource 包装
/**
* 认证逻辑
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证逻辑");
// 编写shiro判断逻辑,判断用户名和密码
System.out.println(authenticationToken.toString());
// 1. 判断用户名
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
User byUsername = userService.getByUsername(username);
if (byUsername == null) {
//用户名不存在
return null;//shiro底层会抛出UnknownAccountException
}
// 2. 判断密码
// 参数1:需要返回给login方法的数据;参数2:数据库密码,shiro会自动判断 参数3:密码加密的盐
return new SimpleAuthenticationInfo(byUsername, byUsername.getPassword(), ByteSourceUtils.bytes(username) , getName());
}
修改shiro配置类 的ream bean配置
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
// 设置加密算法
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher("md5");
// 设置加密次数
credentialsMatcher.setHashIterations(1);
userRealm.setCredentialsMatcher(credentialsMatcher);
//开启缓存,设置缓存管理器
userRealm.setCachingEnabled(true);
userRealm.setAuthenticationCachingEnabled(true);
userRealm.setAuthorizationCachingEnabled(true);
userRealm.setCacheManager(new RedisCacheManager());
return userRealm;
}