0.准备
SQL使用flyway管理, DAO层是用mybatis, 缓存使用Redis, 具体看源码(本章末尾会贴出).下面主要是shiro部分
1.添加Maven依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
2.自定义SpringbootDemoAuthorizingRealm继承AuthorizingRealm
public class SpringbootDemoAuthorizingRealm extends AuthorizingRealm {
@Autowired
private IUserService userServiceImpl;
@Autowired
private IRoleService roleServiceImpl;
@Autowired
private IPermissionService permissionServiceImpl;
@Override
public String getName() {
return "SpringbootDemoAuthorizingRealm";
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
String password = new String(token.getPassword());
if (StringUtils.isBlank(username)) {
throw new UnknownAccountException();
}
if (StringUtils.isBlank(password)) {
throw new IncorrectCredentialsException();
}
User user = userServiceImpl.getUserByUsername(username);
if (Objects.isNull(user)) {
throw new UnknownAccountException();
}
SimpleUser simpleUser = new SimpleUser();
simpleUser.setId(user.getId());
simpleUser.setUsername(user.getUsername());
// 清除授权信息
clearAuthorizationInfoCache(simpleUser);
return new SimpleAuthenticationInfo(simpleUser, user.getPassword(), ByteSource.Util.bytes(username), this.getName());
}
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleUser simpleUser = (SimpleUser) principalCollection.getPrimaryPrincipal();
List<Role> roleList = roleServiceImpl.getRoleListByUserId(simpleUser.getId());
List<String> simpleRoles = new ArrayList<>(roleList.size());
List<Long> roleIds = new ArrayList<>(roleList.size());
for (Role role : roleList) {
simpleRoles.add(role.getRole());
roleIds.add(role.getId());
}
List<String> permissions = permissionServiceImpl.getPermissionsByRoleIds(roleIds);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(simpleRoles);
simpleAuthorizationInfo.addStringPermissions(permissions);
return simpleAuthorizationInfo;
}
/**
* 清除所有授权信息
*/
public void clearAuthorizationInfoCache() {
Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
if(Objects.nonNull(cache)) {
cache.clear();
}
}
private void clearAuthorizationInfoCache(SimpleUser simpleUser) {
Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
cache.remove(simpleUser.getId());
}
}
3.自定义ShiroConfig
@Configuration
public class ShiroConfig {
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(3);
return hashedCredentialsMatcher;
}
@Bean
public SpringbootDemoAuthorizingRealm springbootDemoAuthorizingRealm(HashedCredentialsMatcher hashedCredentialsMatcher) {
SpringbootDemoAuthorizingRealm realm = new SpringbootDemoAuthorizingRealm();
realm.setCredentialsMatcher(hashedCredentialsMatcher);
// 启用缓存
realm.setCachingEnabled(true);
// 启用认证缓存
realm.setAuthorizationCachingEnabled(true);
// 启用授权缓存
realm.setAuthorizationCachingEnabled(true);
return realm;
}
@Bean
public SecurityManager securityManager(SpringbootDemoAuthorizingRealm springbootDemoAuthorizingRealm, SessionManager sessionManager, ShiroCacheManager shiroCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(springbootDemoAuthorizingRealm);
securityManager.setSessionManager(sessionManager);
securityManager.setCacheManager(shiroCacheManager);
return securityManager;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/");
shiroFilterFactoryBean.setUnauthorizedUrl("/forbidden.do");
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
// 登录不拦截
filterChainDefinitionMap.put("/login.do", "anon");
// 其他接口都必须登录
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
// 跳转页面删除sessionId
defaultWebSessionManager.setSessionIdUrlRewritingEnabled(false);
// 30分钟
defaultWebSessionManager.setGlobalSessionTimeout(1800000);
defaultWebSessionManager.setDeleteInvalidSessions(true);
defaultWebSessionManager.setSessionValidationSchedulerEnabled(true);
defaultWebSessionManager.setSessionIdCookieEnabled(true);
return defaultWebSessionManager;
}
}
4.自定义ShiroCacheManager, 使用Redis作为Shiro缓存
@Component
public class ShiroCacheManager implements CacheManager {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
return new ShiroRedisCache<>(name);
}
public class ShiroRedisCache<K,V> implements Cache<K,V> {
private String cacheKey;
private ShiroRedisCache(String cacheKey) {
this.cacheKey = cacheKey;
}
private Object hashKey(K key) {
if(key instanceof PrincipalCollection) {
PrincipalCollection principalCollection = (PrincipalCollection) key;
SimpleUser simpleUser = (SimpleUser) principalCollection.getPrimaryPrincipal();
return simpleUser.getId();
}
return key;
}
@Override
public V get(K key) throws CacheException {
BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
Object realKey = hashKey(key);
return boundHashOperations.get(realKey);
}
@Override
public V put(K key, V value) throws CacheException {
BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
Object realKey = hashKey(key);
boundHashOperations.put((K) realKey, value);
return value;
}
@Override
public V remove(K key) throws CacheException {
BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
Object realKey = hashKey(key);
V value = boundHashOperations.get(realKey);
boundHashOperations.delete(realKey);
return value;
}
@Override
public void clear() throws CacheException {
redisTemplate.delete(cacheKey);
}
@Override
public int size() {
BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
return boundHashOperations.size().intValue();
}
@Override
public Set<K> keys() {
BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
return boundHashOperations.keys();
}
@Override
public Collection<V> values() {
BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
return boundHashOperations.values();
}
}
}
5.使用
@RestController
public class LoginController {
@RequestMapping(value="", method = RequestMethod.GET)
public ResultMessage defaultPath(){
return ResultMessage.fail("请先登录");
}
@RequestMapping(value="/forbidden.do", method = RequestMethod.GET)
public ResultMessage forbidden(){
return ResultMessage.fail("没有权限");
}
@RequestMapping(value="/login.do", method = RequestMethod.POST)
public ResultMessage login(@RequestBody UserRequest request){
UsernamePasswordToken token = new UsernamePasswordToken(request.getUsername(), request.getPassword(),false);
Subject currentUser = SecurityUtils.getSubject();
try {
currentUser.login(token);
} catch (UnknownAccountException e) {
throw new UnknownAccountException("账号不存在");
} catch (IncorrectCredentialsException e) {
throw new IncorrectCredentialsException("密码不匹配");
} catch (LockedAccountException e) {
throw new LockedAccountException("账号被锁定");
} catch (AuthenticationException e) {
throw new AuthenticationException("其他认证错误");
} catch (RuntimeException e) {
throw new RuntimeException("登录未知错误");
}
return ResultMessage.success("登录成功");
}
@RequestMapping(value="/logout.do", method =RequestMethod.GET)
public ResultMessage logout(){
try {
SecurityUtils.getSubject().logout();
} catch (Exception e) {
throw new RuntimeException("注销未知错误");
}
return ResultMessage.success("注销成功");
}
@RequestMapping(value="/other.do", method =RequestMethod.GET)
public ResultMessage other(){
return ResultMessage.success("other success.");
}
@RequestMapping(value="/addTeacher.do", method =RequestMethod.GET)
@RequiresPermissions({"teacher:add"})
public ResultMessage addTeacher(){
return ResultMessage.success("permission success.");
}
@RequestMapping(value="/updateTeacher.do", method =RequestMethod.GET)
@RequiresPermissions({"teacher:update"})
public ResultMessage updateTeacher(){
return ResultMessage.success("permission success.");
}
/**
* 统一异常处理
*/
@ExceptionHandler(RuntimeException.class)
public ResultMessage exception(Exception ex) {
if (ex instanceof AuthorizationException) {
return ResultMessage.fail("未授权");
}
return ResultMessage.fail(ex.getMessage());
}
}
6.总结
shiro相对比较简单, 认证(可以理解为登录), 授权(就是权限判断), 会话管理, 加密等模块.
下一节会讲如何重写注解RequiresPermissions, 并将菜单入库.
源码 https://gitee.com/jsjack_wang/springboot-demo dev-shiro分支