一、引言
上一篇,我们对自定义权鉴进行了实现,但是在很多的登录场景中,用户名和密码不是仅有的用于登录认证授权的字段,会有各类的情况。比如:学校系统登录时需要加选学生或老师身份,多组织系统登录时需要选择组织(域)等等。所以这篇文章,我们就是来用SS实现这类场景登录。
二、首先需要了解的
1、Sping Security的作用流程
2、默认的过滤器
别名 | 过滤器类 Filter Class | 命名空间元素或属性 |
---|---|---|
CHANNEL_FILTER | ChannelProcessingFilter | http/intercept-url@requires-channel |
CONCURRENT_SESSION_FILTER | ConcurrentSessionFilter | session-management/concurrency-control |
SECURITY_CONTEXT_FILTER | SecurityContextPersistenceFilter | http |
LOGOUT_FILTER | LogoutFilter | http/logout |
X509_FILTER | X509AuthenticationFilter | http/x509 |
PRE_AUTH_FILTER | AstractPreAuthenticatedProcessingFilter Subclasses | N/A |
CAS_FILTER | CasAuthenticationFilter | N/A |
FORM_LOGIN_FILTER | UsernamePasswordAuthenticationFilter | http/form-login |
BASIC_AUTH_FILTER | BasicAuthenticationFilter | http/http-basic |
SERVLET_API_SUPPORT_FILTER | SecurityContextHolderAwareFilter | http/@servlet-api-provision |
REMEMBER_ME_FILTER | RememberMeAuthenticationFilter | http/remember-me |
ANONYMOUS_FILTER | AnonymousAuthenticationFilter | http/anonymous |
SESSION_MANAGEMENT_FILTER | SessionManagementFilter | session-management |
EXCEPTION_TRANSLATION_FILTER | ExceptionTranslationFilter | http |
FILTER_SECURITY_INTERCEPTOR | FilterSecurityInterceptor | http |
SWITCH_USER_FILTER | SwitchUserFilter | N/A |
验证过程按照这样顺序自上而下进行。
3、自定义过滤器位置
在spring-security.xml配置中,需要根据自定义过滤器的场景需要将过滤器放置在相关的位置上。
根据上表的顺序来指定。
before:在对应过滤器之前;
after:在对应过滤器之后;
position:在对应过滤器上。
三、代码实现
1、实现思路
用Spring Security提供的实现(DaoAuthenticationProvider和UsernamePasswordToken)。
关键组件包括:
SimpleAuthenticationFilter - UsernamePasswordAuthenticationFilter的扩展
SimpleUserDetailsService - UserDetailsService的实现
User - Spring Security提供的User类的扩展,它声明了我们的额外域字段
配置Spring Security,将SimpleAuthenticationFilter插入到过滤器链中,声明安全规则并连接依赖项
login.html - 收集用户名,密码和组织域的登录页面
2、主要代码
代码略去空间命名已经import部分。
Authentication Filter
SimpleAuthenticationFilter中,组织域和用户名字段是从请求中提取。连接这些值并使用它们来创建UsernamePasswordAuthenticationToken的实例。
public class OursPlusAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private String organizationParameter = "organization";
protected String obtainOrganization(HttpServletRequest request) {
return request.getParameter(this.organizationParameter);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
UsernamePasswordAuthenticationToken authRequest = getAuthRequest(request);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private UsernamePasswordAuthenticationToken getAuthRequest(HttpServletRequest request) {
String username = obtainUsername(request);
String password = obtainPassword(request);
String organization = obtainOrganization(request);
String usernameOrganization = String.format("%s%s%s", username.trim(), String.valueOf(Character.LINE_SEPARATOR), organization);
return new UsernamePasswordAuthenticationToken(usernameOrganization, password);
}
}
其中“organization”就是新增的组织域字段。
UserDetails服务
UserDetailsService的loadUserByUsername方法,改造实现提取用户名和组织域。然后将值传递给UserRepository以获取用户。
public class OursPlusUserDetailsService implements UserDetailsService {
@Autowired
private IUsersService usersService;
@Autowired
private OursUser oursUser;
@Override
public UserDetails loadUserByUsername(String plusUsername) throws UsernameNotFoundException {
//整合organization后相关处理
String[] usernameOrganization = StringUtils.split(plusUsername, String.valueOf(Character.LINE_SEPARATOR));
if (usernameOrganization == null || usernameOrganization.length != 2) {
throw new UsernameNotFoundException("需完善用户名及企业组织完善");
}
//查询用户
Users users = usersService.findByUsername(usernameOrganization[0]);
if (users == null) {
throw new UsernameNotFoundException("用户不存在");
}
//组织id
long organizationId = Integer.parseInt(usernameOrganization[1]);
//一般用户权限获取
List<String> roles = usersService.findRolesCodeByUserIdAndOrgId(users.getId(), organizationId);
List<String> rules = usersService.findRuleNameByUserIdAndOrgId(users.getId(), organizationId);
List<String> authorities;
authorities = oursUser.getAuthorities(users.getId(), roles, rules);
User user = new User(users.getUsername(), users.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", authorities)));
return user;
}
}
oursUser类
额外的自定义类,主要是封装一些出来方法处理一些数据库权鉴,本文章的代码需要基于上一篇文章《(Spring Security)实战干货一:自定义权鉴的实现》的代码,主要是理解基于数据库的认证与授权。
public class OursUser {
@Autowired
private IUsersService usersService;
/**
* 获取当前登录用户名
*
* @return
*/
public String getLoginUserName() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User principal = (User) authentication.getPrincipal();
return principal.getUsername();
}
/**
* 获取当前登录用户所有信息(除密码外)
*
* @return
*/
public Users get() {
Users users;
String username = getLoginUserName();
if (username.length() != 0) {
users = usersService.findUserInfoByUsername(username);
} else {
users = null;
}
return users;
}
/**
* 获取权限配置
* @param userId
* @param roles
* @param rules
* @return
*/
public List<String> getAuthorities(Integer userId,List<String> roles,List<String> rules){
List<String> authorities=new ArrayList<>();
if(userId==1){
List<String> adminRules=usersService.findAllRuleName();
authorities.add("ROLE_ADMIN");
authorities.addAll(adminRules);
}else{
roles=roles.stream()
.map(prc->"ROLE_"+prc)
.collect(Collectors.toList());
authorities.addAll(roles);
authorities.addAll(rules);
}
return authorities;
}
}
以上存在笔者设定的权限逻辑,可以忽略,按自己的需求设定。
3、SS配置
...
<security:custom-filter ref="customFilters" before="FORM_LOGIN_FILTER"/>
...
<bean id="oursPlusAuthenticationFilter" class="index.OursPlusAuthenticationFilter"/>
<bean id="customFilters" class="org.springframework.web.filter.CompositeFilter">
<property name="filters">
<list>
<ref bean="oursPlusAuthenticationFilter"/>
</list>
</property>
</bean>
...
以上配置文件是截取了部分配置信息,可结合上一篇文章,了解配置信息。
4、登录页面
<form autoComplete="off" action="/" method="post">
<div>
<div>
<label>用户名</label>
<input type="text" name="username" placeholder="用户名">
</div>
<div>
<label>密码</label>
<input type="password" name="password" placeholder="密码">
</div>
<div>
<label>组织域</label>
<input type="text" name="organization" placeholder="组织域">
</div>
<div>
<input type="submit" value="登 入">
</div>
</div>
</form>
以上是实现的主要思路及代码,希望对大家有帮助,同时不足之处,希望指出。