Мне нужно добавить в форму авторизации третье поле, но не соображу, как научить Spring Security с ним работать. У меня пользователи лежат в базе, но кроме имени и пароля имеют ещё один признак, вроде домена. Смутно догадываюсь, что проблема состоит из двух частей:
Уже почти неделю бьюсь. На два раза перечитал Spring Security Reference и с головой закопался в javadoc по API. Но или там этого нет, или у меня знаний и опыта не хватает для понимания.
Возможно ли это? Если да, то как такое сделать?
Документация к Spring Security уже много лет держит третье место в моём личном рейтинге злых и бесполезных документов. Когда я столкнулся с подобной задачей, пришлось лезть в исходный код Spring Security, чтобы разобраться. И до сих пор не уверен, что всё сделал правильно.
Для реализации вашей задачи потребуется как минимум переопределить WebAuthenticationDetailsSource, WebAuthenticationDetails, AuthenticationProvider и UserDetailsService. Чтобы сэкономить время и силы, наследовать два последних будем от DaoAuthenticationProvider и JdbcDaoImpl, соответственно.
RealmAuthenticationDetailsSource.java
@Component("authenticationDetailSource")
public class RealmAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new RealmAuthenticationDetails(context);
}
}
RealmAuthenticationDetails.java
public class RealmAuthenticationDetails extends WebAuthenticationDetails {
private final String realm;
public RealmAuthenticationDetails(HttpServletRequest context) {
super(context);
String realm = context.getParameter("realm");
this.realm = realm != null ? realm : "";
}
public String getRealm() {
return realm;
}
}
RealmAuthenticationProvider.java
@Service("authenticationProvider")
public class RealmAuthenticationProvider extends DaoAuthenticationProvider {
@Autowired
private RealmedUserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@PostConstruct
private void initialize() {
setUserDetailsService(userDetailsService);
setPasswordEncoder(passwordEncoder);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
RealmAuthenticationDetails details = (RealmAuthenticationDetails) authentication.getDetails();
String username = authentication.getName();
String password = authentication.getCredentials().toString();
String realm = details.getRealm();
User user = (User) userDetailsService.loadUserByUsernameAndRealm(username, realm);
if (user == null) {
throw new BadCredentialsException("Username not found");
}
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("Wrong password");
}
return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
}
}
RealmedUserDetailsService.java
@Service("userDetailsService")
public class RealmedUserDetailsService extends JdbcDaoImpl {
private final boolean enableGroups = true;
private final boolean enableAuthorities = false;
private final String rolePrefix = "ROLE_";
private final String usersByUsernameQuery
= "select login, password, enabled from employee where login = ?";
private final String usersByUsernameAndRealmQuery
= "select e.login, e.password, e.enabled from groups_employee as m2m "
+ "inner join employee as e on e.id = m2m.members_id "
+ "inner join groups as g on g.id = m2m.groups_id "
+ "where e.login = ? and g.company_realm = ?";
private final String authoritiesByUsernameQuery
= "select e.login, g.authority_name from groups_employee as m2m "
+ "inner join employee as e on e.id = m2m.members_id "
+ "inner join groups as g on g.id = m2m.groups_id "
+ "where e.login = ?";
private final String authoritiesByUsernameAndRealmQuery
= "select e.login, g.authority_name from groups_employee as m2m "
+ "inner join employee as e on e.id = m2m.members_id "
+ "inner join groups as g on g.id = m2m.groups_id "
+ "where e.login = ? and g.company_realm = ?";
private final String groupAuthoritiesByUsernameQuery
= "select e.login, g.name, g.authority_name from groups_employee as m2m "
+ "inner join employee as e on e.id = m2m.members_id "
+ "inner join groups as g on g.id = m2m.groups_id "
+ "where e.login = ?";
private final String groupAuthoritiesByUsernameAndRealmQuery
= "select e.login, g.name, g.authority_name from groups_employee as m2m "
+ "inner join employee as e on e.id = m2m.members_id "
+ "inner join groups as g on g.id = m2m.groups_id "
+ "where e.login = ? and g.company_realm = ?";
@Autowired
private DataSource dataSource;
protected void initialize() {
setEnableGroups(enableGroups);
setEnableAuthorities(enableAuthorities);
setRolePrefix(rolePrefix);
setUsersByUsernameQuery(usersByUsernameQuery);
setAuthoritiesByUsernameQuery(authoritiesByUsernameQuery);
setGroupAuthoritiesByUsernameQuery(groupAuthoritiesByUsernameQuery);
setDataSource(dataSource);
}
public UserDetails loadUserByUsernameAndRealm(String username, String realm) {
List<UserDetails> users = loadUsersByUsernameAndRealm(username, realm);
if (users.size() == 0) {
throw new UsernameNotFoundException("Username not found");
}
UserDetails user = users.get(0);
List<GrantedAuthority> authorities = loadGroupAuthoritiesForRealm(user.getUsername(), realm);
return createUserDetails(username, user, authorities);
}
protected List<UserDetails> loadUsersByUsernameAndRealm(String username, String realm) {
return getJdbcTemplate().query(usersByUsernameAndRealmQuery, new String[] { username, realm },
new RowMapper<UserDetails>() {
public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
String username = rs.getString(1);
String password = rs.getString(2);
boolean enabled = rs.getBoolean(3);
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
return new User(username, password, enabled, accountNonExpired, credentialsNonExpired,
accountNonLocked, AuthorityUtils.NO_AUTHORITIES);
}
}
);
}
protected List<GrantedAuthority> loadGroupAuthoritiesForRealm(String username, String realm) {
return getJdbcTemplate().query(groupAuthoritiesByUsernameAndRealmQuery, new String[] { username, realm },
new RowMapper<GrantedAuthority>() {
public GrantedAuthority mapRow(ResultSet rs, int rowNum) throws SQLException {
String roleName = getRolePrefix() + rs.getString(3);
return new SimpleGrantedAuthority(roleName);
}
}
);
}
}
security-context.xml
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<http use-expressions="true" request-matcher="regex">
<intercept-url pattern="/admin/.*" access="hasRole('ROLE_ADMIN')" />
<form-login
login-page="/auth/login"
authentication-failure-url="/auth/login?fail"
default-target-url="/"
login-processing-url="/auth/login"
authentication-details-source-ref="authenticationDetailSource" />
<logout
logout-url="/auth/logout"
logout-success-url="/" />
</http>
<global-method-security pre-post-annotations="enabled"/>
<authentication-manager>
<authentication-provider ref="authenticationProvider" />
</authentication-manager>
<beans:bean name="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
</beans:beans>
Скорее всего, у вас другая структура БД, запросы надо подредактировать под себя. Все параметры UserDetailsService
можно вынести в конфигурацию, если вам так будет удобнее. И рекомендую организовать кэширование UserDetails
, чтобы не дёргать БД на каждом запросе.
Айфон мало держит заряд, разбираемся с проблемой вместе с AppLab
Перевод документов на английский язык: Важность и ключевые аспекты
ПроектНапишите систему, использующую динамические заместители для реализации транзакций: заместитель закрепляет транзакцию, если опосредованный...