Я недавно столкнулся с такой проблемой в проекте: front-end и back-end разделены, front-end делается на Vue, все запросы данных используют vue-resource, а формы не используются, поэтому взаимодействие с данными использует JSON, серверная часть использует Spring Boot, а проверка разрешений использует Spring Security, потому что раньше я использовал Spring Security для обработки страниц, в этот раз я обрабатывал только Ajax-запросы, поэтому зафиксировал некоторые возникшие проблемы. Решение здесь не только для Ajax-запросов, но и для проверки мобильных запросов.
Создать проект
Прежде всего, нам нужно создать проект Spring Boot, который должен представить Web, Spring Security, MySQL и MyBatis (инфраструктура базы данных на самом деле необязательна, здесь я использую MyBatis).После создания файлы зависимостей выглядят следующим образом:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
Обратите внимание на последнееcommons-codecЗависимости добавляются мной вручную.Это проект Apache с открытым исходным кодом, который можно использовать для создания дайджестов сообщений MD5.Я проведу простую обработку паролей в следующих разделах.
Создать базу данных и настроить
Чтобы упростить логику, я создал здесь три таблицы, а именно таблицу пользователей, таблицу ролей и таблицу сопоставления ролей пользователей, как показано ниже:
Далее нам нужно просто настроить собственную базу данных в application.properties, Здесь каждый малый партнер зависит от своей конкретной ситуации.
spring.datasource.url=jdbc:mysql:///vueblog
spring.datasource.username=root
spring.datasource.password=123
Построить класс сущностей
В основном это относится к конструкции пользовательского класса. Пользовательский класс здесь особенный и должен реализовывать интерфейс UserDetails следующим образом:
public class User implements UserDetails {
private Long id;
private String username;
private String password;
private String nickname;
private boolean enabled;
private List<Role> roles;
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public List<GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
}
return authorities;
}
//getter/setter省略...
}
После реализации интерфейса UserDetails в этом интерфейсе есть несколько методов, которые нам необходимо реализовать. Четыре метода, которые возвращают логическое значение, хорошо известны, а enable указывает, включена ли учетная запись расписания. Это поле существует в моей базе данных, поэтому, согласно к результату запроса Возврат, по другим причинам, для простоты верните true напрямую. Метод getAuthorities возвращает информацию о роли текущего пользователя. Роль пользователя — это фактически данные в ролях. Преобразуйте данные в ролях в ListROLE_
начинается с такого символа, поэтому вам нужно вручную добавить его сюдаROLE_
, Помните.
Существует также класс сущности Роль, который относительно прост и может быть создан в соответствии с полями базы данных, которые здесь не будут повторяться.
Создать пользовательский сервис
UserService здесь тоже особенный и должен реализовать интерфейс UserDetailsService следующим образом:
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Autowired
RolesMapper rolesMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(s);
if (user == null) {
//避免返回null,这里返回一个不含有任何值的User对象,在后期的密码比对过程中一样会验证失败
return new User();
}
//查询用户的角色信息,并返回存入user中
List<Role> roles = rolesMapper.getRolesByUid(user.getId());
user.setRoles(roles);
return user;
}
}
После реализации интерфейса UserDetailsService нам нужно реализовать в интерфейсе метод loadUserByUsername, то есть опрашивать пользователей по их логинам. Здесь вводятся два Mapper в MyBatis, UserMapper используется для запроса пользователей, а RolesMapper используется для запроса ролей. В методе loadUserByUsername сначала запрашивается пользователь в соответствии с входящим параметром (параметром является имя пользователя, введенное при входе пользователя в систему). Если найденный пользователь является нулем, исключение UsernameNotFoundException может быть сгенерировано напрямую, но для удобства обработки Я возвращаю объект пользователя без какого-либо значения, так что ошибка входа будет обнаружена в последующем процессе сравнения паролей (здесь вы можете настроить его в соответствии с вашими потребностями бизнеса).Если найденный пользователь не является нулевым, мы найдем его в соответствии с Затем запросите роль пользователя и поместите результат запроса в объект пользователя.Результат запроса будет использоваться в методе getAuthorities объекта пользователя.
Конфигурация безопасности
Давайте сначала посмотрим на мою конфигурацию безопасности, а затем я объясню ее по порядку:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
/**
* @param charSequence 明文
* @param s 密文
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()));
}
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("超级管理员")
.anyRequest().authenticated()//其他的路径都是登录后即可访问
.and().formLogin().loginPage("/login_page").successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write("{\"status\":\"ok\",\"msg\":\"登录成功\"}");
out.flush();
out.close();
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write("{\"status\":\"error\",\"msg\":\"登录失败\"}");
out.flush();
out.close();
}
}).loginProcessingUrl("/login")
.usernameParameter("username").passwordParameter("password").permitAll()
.and().logout().permitAll().and().csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/reg");
}
}
Это ядро нашей конфигурации, друзья, слушайте меня по порядку:
1. Прежде всего, это класс конфигурации, поэтому не забудьте добавить аннотацию @Configuration, а поскольку это конфигурация Spring Security, не забудьте наследовать WebSecurityConfigurerAdapter.
2. Вставьте только что созданный UserService, мы будем использовать его позже.
3. Метод configure(AuthenticationManagerBuilder auth) используется для настройки нашего метода аутентификации, а userService передается в методе auth.userDetailsService(), так что метод loadUserByUsername в userService будет автоматически вызываться при входе пользователя в систему. Последний passwordEncoder является необязательным, его можно записать или нет, потому что я сгенерировал дайджест сообщения MD5 открытого текста пароля пользователя и сохранил его в базе данных, поэтому мне также нужно обрабатывать открытый текстовый пароль при входе в систему, поэтому я добавил passwordEncoder, После добавления passwordEncoder вы можете напрямую создать новый анонимный внутренний класс PasswordEncoder. Есть два метода для реализации. Вы можете узнать значение метода, взглянув на имя. Первый метод, encode, очевидно, предназначен для шифрования открытого текста. Здесь я использую дайджест сообщения MD5. , конкретный метод реализации обеспечивается зависимостью commons-codec; второй метод соответствует — это сравнение пароля, два параметра, первый параметр — открытый текстовый пароль, второй — зашифрованный текст, только здесь требуется шифрование открытым текстом. Затем сравните его с зашифрованным текстом (если вас это интересует, вы можете продолжить рассмотрение вопроса о добавлении соли к паролю).
4.configure(HttpSecurity http) используется для настройки наших правил аутентификации и т. д. Метод authorizeRequests указывает, что конфигурация правил аутентификации включена, а antMatchers("/admin/**").hasRole("суперадминистратор") указывает, что/admin/**
Путь должен быть доступен пользователю с ролью «суперадминистратор».Я видел в Интернете, что мой партнер должен добавить в метод hasRole.ROLE_
Если префикс вызывает сомнения, его здесь добавлять не следует, только если используется метод hasAuthority. anyRequest().authenticated() указывает, что все остальные пути требуют аутентификации/логина, прежде чем к ним можно будет получить доступ. Затем мы настраиваем страницу входа как login_page, путь обработки входа как /login, имя пользователя для входа как имя пользователя и пароль как пароль, и настраиваем эти пути для прямого доступа, выход из системы и вход в систему для прямого доступа и, наконец, закрыть csrf. В SuccessHandler вы можете использовать response для возврата json успешного входа. Помните, что не следует использовать defaultSuccessUrl. DefaultSuccessUrl — это страница, перенаправляемая только после успешного входа в систему. FailureHandler также используется по той же причине.
5. В методе configure(WebSecurity web) я настроил некоторые правила фильтрации, поэтому не буду вдаваться в подробности.
6. Кроме того, для статических файлов, таких как/images/**
,/css/**
,/js/**
Эти пути здесь не заблокированы по умолчанию.
Controller
Наконец, давайте посмотрим на наш контроллер следующим образом:
@RestController
public class LoginRegController {
/**
* 如果自动跳转到这个页面,说明用户未登录,返回相应的提示即可
* <p>
* 如果要支持表单登录,可以在这个方法中判断请求的类型,进而决定返回JSON还是HTML页面
*
* @return
*/
@RequestMapping("/login_page")
public RespBean loginPage() {
return new RespBean("error", "尚未登录,请登录!");
}
}
Контроллер относительно прост в целом, RespBean является компонентом ответа и возвращает простой json, не буду вдаваться в подробности, здесь нужно обратить внимание на то, чтоlogin_page
, страница входа, которую мы настроили, представляет собойlogin_page
,Но по фактуlogin_page
Не страница, а кусок JSON, потому что, когда я перехожу на другие страницы без входа в систему, Spring Security автоматически переходит наlogin_page
page, но в запросе Ajax такой переход не нужен, все, что я хочу, это подсказка, следует ли войти в систему, поэтому просто верните json здесь.
контрольная работа
Наконец, друзья могут использовать такие инструменты, как POSTMAN или RESTClient, для проверки проблем с входом в систему и разрешениями, и я не буду их демонстрировать.
Хорошо, после приведенного выше введения вы должны иметь некоторое представление о том, как Spring Boot + Spring Security обрабатывает запросы на вход Ajax. Ну, это все для этой статьи. Если у вас есть какие-либо вопросы, оставьте сообщение для обсуждения.
Для получения дополнительной информации, пожалуйста, обратите внимание на общедоступный номер: