虽然,直接用Spring Security和SpringBoot 进行“全家桶式”的合作是最好不过的,但现实总是欺负我们这些没办法决定架构类型的娃子。
Apache Shiro 也有其特殊之处滴。若需了解,可以转战到[Apache Shiro 简介]
1. 添加Shiro依赖
shiro的版本,看个人喜好哈,本文的版本为:
<shiro.version>1.3.2</shiro.version>
复制代码
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-aspectj</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
复制代码
2. shiroRealm
授权认证具体实现之地。通过继承 AuthorizingRealm 进而实现,对登录时的账号密码校验功能
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private ShiroPermissionRepository shiroPermissionRepository;
/**
* 授权
*
* @param principalCollection 主要信息
* @return 授权信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
if (log.isInfoEnabled()){
log.info("Authorization begin");
}
String name= (String) principalCollection.getPrimaryPrincipal();
List<String> role = shiroPermissionRepository.queryRoleByName(name);
if (role.isEmpty()){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(role);
return simpleAuthorizationInfo;
}
return null;
}
/**
* 认证
*
* @param authenticationToken 认证token
* @return 认证结果
* @throws AuthenticationException 认证异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (log.isInfoEnabled()){
log.info("Authentication begin");
}
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
Object principal =token.getPrincipal();
Object credentials = token.getCredentials();
//校验用户名
checkBlank(principal,"用户名不能为空");
//校验密码
checkBlank(credentials,"密码不能为空");
//校验姓名
String username = (String) principal;
UserPO userPO = shiroPermissionRepository.findAllByName(username);
if (userPO == null){
throw new AccountException("用户名错误");
}
//校验密码
String password = (String) credentials;
if (!StringUtils.equals(password,userPO.getPassword())){
throw new AccountException("密码错误");
}
return new SimpleAuthenticationInfo(principal, password, getName());
}
private void checkBlank(Object obj,String message){
if (obj instanceof String){
if (StringUtils.isBlank((String) obj)){
throw new AccountException(message);
}
}else if (obj == null){
throw new AccountException(message);
}
}
}
复制代码
3. 配置ShiroConfig
将ShiroConfig、SecurityManager、ShiroFilterFactoryBean交给Spring管理.
- ShiroRealm: 则上述所描述的ShiroRealm
- SecurityManager: 管理 所有用户 的安全操作
- ShiroFilterFactoryBean: 配置Shiro的过滤器
@Configuration
public class ShiroConfig {
private final static String AUTHC_STR = "authc";
private final static String ANON_STR = "anon";
/**
* 验证授权、认证
*
* @return shiroRealm 授权认证
*/
@Bean
public ShiroRealm shiroRealm(){
return new ShiroRealm();
}
/**
* session manager
*
* @param shiroRealm 授权认证
* @return 安全管理
*/
@Bean
@ConditionalOnClass(ShiroRealm.class)
public SecurityManager securityManager(ShiroRealm shiroRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm);
return securityManager;
}
/**
* Filter工厂,设置对应的过滤条件和跳转条件
*
* @param securityManager session 管理
* @return shiro 过滤工厂
*/
@Bean
@ConditionalOnClass(value = {SecurityManager.class})
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setFilters(filterMap);
//URI过滤
Map<String,String> map = Maps.newLinkedHashMap();
//可过滤的接口路径
//所有API路径进行校验
map.put("/api/**",AUTHC_STR);
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
}
复制代码
3.1 Shiro 过滤器小插曲
shiro和security也有相似之处,都有自己的 filter chain。翻一番Shiro的源码,追溯一下。发下以下:
3.1.1 ShiroFilterFactoryBean——createFilterChainManager
protected FilterChainManager createFilterChainManager() {
DefaultFilterChainManager manager = new DefaultFilterChainManager();
Map<String, Filter> defaultFilters = manager.getFilters();
//apply global settings if necessary:
for (Filter filter : defaultFilters.values()) {
applyGlobalPropertiesIfNecessary(filter);
}
//Apply the acquired and/or configured filters:
Map<String, Filter> filters = getFilters();
if (!CollectionUtils.isEmpty(filters)) {
for (Map.Entry<String, Filter> entry : filters.entrySet()) {
String name = entry.getKey();
Filter filter = entry.getValue();
applyGlobalPropertiesIfNecessary(filter);
if (filter instanceof Nameable) {
((Nameable) filter).setName(name);
}
//'init' argument is false, since Spring-configured filters should be initialized
//in Spring (i.e. 'init-method=blah') or implement InitializingBean:
manager.addFilter(name, filter, false);
}
}
//build up the chains:
Map<String, String> chains = getFilterChainDefinitionMap();
if (!CollectionUtils.isEmpty(chains)) {
for (Map.Entry<String, String> entry : chains.entrySet()) {
String url = entry.getKey();
String chainDefinition = entry.getValue();
manager.createChain(url, chainDefinition);
}
}
return manager;
}
复制代码
从源码可以发现,shiro的过滤器链,添加顺序是:
- defaultFilters: shiro默认的过滤器链
- filters: 咱们自定义的过滤器链
- chains:明确指定要过滤的
3.1.2 DefaultFilterChainManager —— addDefaultFilters
这里咱看看DefaultFilterChainManager 到底添加了那些默认过滤器链,可以看到主要的是:DefaultFilter
protected void addDefaultFilters(boolean init) {
for (DefaultFilter defaultFilter : DefaultFilter.values()) {
addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
}
}
复制代码
3.1.3 DefaultFilter
anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);
复制代码
4. 测它
由于设置对全局接口进行校验,因此,预期结果就是不能够访问啦
map.put("/api/**",AUTHC_STR);
复制代码
4.1 IDAL
@RestController
@RequestMapping( SYSTEM_API +"shiro")
public class ShiroIdal {
@Resource
private IShiroService iShiroService;
@GetMapping
public HttpEntity obtain(@RequestParam String name){
return iShiroService.obtainUserByName(name);
}
}
复制代码
4.2 service
@Slf4j
@Service
public class ShiroServiceImpl implements IShiroService {
@Resource
private ShiroPermissionRepository shiroPermissionRepository;
public HttpEntity obtainUserByName(String name) {
UserPO userPO = shiroPermissionRepository.findAllByName(name);
return HttpResponseSupport.success(userPO);
}
}
复制代码
4.3 被劫持的情况
若没 login.jsp,则会直接报错,个人觉得太不和谐了,毕竟现在都是前后端分离的。
4.4 设置允许访问
在URI过滤Map加入以下:
map.put("/api/shiro",ANON_STR);
复制代码
注意: 要在“全局Api劫持”前添加。而且不要使用“HashMap”,为什么?
4.4.1 HashMap
在说为什么前,先了解HashMap这货是什么原理先。
for (Entry<String, String> entry : hashMap.entrySet()) {
MessageFormat.format("{0}={1}",entry.getKey(),entry.getValue());
}
复制代码
HashMap散列图是按“有利于随机查找的散列(hash)的顺序”。并非按输入顺序。遍历时只能全部输出,而没有顺序。甚至可以rehash()重新散列,来获得更利于随机存取的内部顺序。
这会影响shiro哪里呢?
Map<String, String> chains = getFilterChainDefinitionMap();
if (!CollectionUtils.isEmpty(chains)) {
for (Map.Entry<String, String> entry : chains.entrySet()) {
String url = entry.getKey();
String chainDefinition = entry.getValue();
manager.createChain(url, chainDefinition);
}
}
复制代码
ShiroFilterFactoryBean 中,在构建shiro的filter chain时,会对我们配置的FilterChainDefinitionMap 进行一次遍历,并且将其添加到DefaultFilterChainManager中。
设想以下,若“全局API劫持”在最前面,那么只要在/api/*裆下的,都早早被劫持了。轮得到配置的 anon 么?若由于HashMap的散列排序导致“全局API劫持”在最前面,emmmm,那玩锤子。
4.4.2 LinkedHashMap
因此,建议使用LinkedHashMap,为啥子?撸源码
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
复制代码
内部类中多了两个Entry,一个记录前方entry,一个记录后方entry,这样的双向链表结构保证了插入顺序的有序。
LinkedHashMap底层是数组加单项链表加双向链表 。
- 数组加单向链表就是HashMap的结构,记录数据用,
- 双向链表,存储插入顺序用。
有点跑偏了,这些大伙肯定都知道滴了......