8.1前后端分离设计
8.2 新建Controller进行测试
package com.study.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @ClassName TestController
* @Description TODO
* @Date 2022/8/9 16:11
* @Version 1.0
*/
@RestController
public class TestController {
@GetMapping("/test")
public String test() {
System.out.println("test is ok!");
return "Test is Ok!";
}
}
启动服务,浏览器访问:http://localhost:8080/test,浏览器输出:Test is Ok!",IDEA控制台输出:test is ok!,表示项目创建成功!
8.3 pom.xml引入依赖
<!--引入Spring Security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--引入数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.7</version>
</dependency>
<!--引入mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<!--引入mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
刷新maven,重启启动服务,浏览器再次访问:http://localhost:8080/test,此时需要进行登录认证,用户名为:user,密码为引入SpringSecurity框架后IDEA控制台产生的随机密码,输入正确的用户名和密码登录成功后,浏览器输出:Test is Ok!",IDEA控制台输出:test is ok!,表示项目创建成功!
8.4application.properties添加配置
# datasource:类型、驱动名、用户名、密码
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
# mybatis配置mapper文件的位置和别名设置
# 注意mapper目录(包)新建时必须使用"/",而不是.
mybatis.mapper-locations=classpath:com/study/mapper/*.xml
mybatis.type-aliases-package=com.study.entity
# log:为了显示mybatis运行SQL语句
logging.level.com.study=debug
8.5创建实体类entity
User
package com.study.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.*;
/**
* @ClassName User
* @Description TODO
* @Date 2022/8/10 0:25
* @Version 1.0
*/
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
private List<Role> roles = new ArrayList<>();//关系属性,用来存储当前用户所有角色信息
//返回权限信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(role -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
authorities.add(simpleGrantedAuthority);
});
return authorities;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonLocked;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Boolean getAccountNonExpired() {
return accountNonExpired;
}
public void setAccountNonExpired(Boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public Boolean getAccountNonLocked() {
return accountNonLocked;
}
public void setAccountNonLocked(Boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public Boolean getCredentialsNonExpired() {
return credentialsNonExpired;
}
public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
}
Role
package com.study.entity;
/**
* @ClassName Role
* @Description TODO
* @Date 2022/8/10 0:25
* @Version 1.0
*/
public class Role {
private Integer id;
private String name;
private String nameZh;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNameZh() {
return nameZh;
}
public void setNameZh(String nameZh) {
this.nameZh = nameZh;
}
}
8.6创建UserDao
package com.study.dao;
import com.study.entity.Role;
import com.study.entity.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* @ClassName UserDao
* @Description TODO
* @Date 2022/8/10 0:27
* @Version 1.0
*/
@Mapper
public interface UserDao {
/**
* 根据用户名查找用户
*
* @param username
* @return
*/
User loadUserByUsername(String username);
/**
* 根据用户id查询一个角色,注意一个用户可能不止一种角色
*
* @param uid
* @return
*/
List<Role> getRolesByUid(Integer uid);
}
8.7创建UserDaoMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.study.dao.UserDao">
<!--loadUserByUsername-->
<select id="loadUserByUsername" resultType="User">
select id,
username,
password,
enabled,
accountNonExpired,
accountNonLocked,
credentialsNonExpired
from user
where username = #{username}
</select>
<!--getRolesByUid
需要将角色表和用户-角色表进行关联查询,查询条件为role.id=user_role.uid
其中,uid是外界传入的参数
-->
<select id="getRolesByUid" resultType="Role">
select r.id,
r.name,
r.name_zh nameZh
from role r,
user_role ur
where r.id = ur.rid
and ur.uid = #{uid}
</select>
</mapper>
8.8创建MyUserDetailsService
package com.study.service;
import com.study.dao.UserDao;
import com.study.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
/**
* @ClassName MyUserDetailsService
* @Description TODO
* @Date 2022/8/10 0:26
* @Version 1.0
*/
@Service
public class MyUserDetailsService implements UserDetailsService {
private final UserDao userDao;
@Autowired
public MyUserDetailsService(UserDao userDao) {
this.userDao = userDao;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.loadUserByUsername(username);
if (ObjectUtils.isEmpty(user))
throw new UsernameNotFoundException("用户不存在");
user.setRoles(userDao.getRolesByUid(user.getId()));
return user;
}
}
8.9 编写前后端分离认证Filter
在进行前后端分离认证时,后端接收到的是前端发送过来的post形式的json数据,此时需要在json格式中获取用于登录的用户名和密码,而SpringSecurity中用于对用户名和密码进行拦截的过滤器为UsernamePasswordAuthenticationFilter,此时需要对此Filter进行覆盖,重写attemptAuthentication()方法后得到用户名和密码,从而进行认证。
package com.study.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
/**
* @ClassName LoginFilter
* @Description 自定义前后端分离Filter
* @Date 2022/8/9 17:42
* @Version 1.0
*/
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("========================================");
//1.判断是否是 post 方式请求
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//2.判断是否是 json 格式请求类型
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
//3.从 json 数据中获取用户输入用户名和密码进行认证 {"uname":"xxx","password":"xxx"}
try {
Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
//父类UsernamePasswordAuthenticationFilter有两个属性usernameParameter和passwordParameter
//在使用构造器注入LoginFilter时可以将自定义两个参数名称
String username = userInfo.get(getUsernameParameter());
String password = userInfo.get(getPasswordParameter());
System.out.println("用户名: " + username + " 密码: " + password);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
//使用父类的AuthenticationManager的authenticate进行认证
return this.getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
e.printStackTrace();
}
}
//如果请求的内容类型不是JSON格式或请求不是POST方式,则调用父类的attemptAuthentication方法进行认证。
//也就是UsernamePasswordAuthenticationFilter的attemptAuthentication方法
return super.attemptAuthentication(request, response);
}
}
8.10完善SpringSecurity配置类
package com.study.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName SecurityConfig
* @Description SpringSecurity配置类
* @Date 2022/8/9 17:36
* @Version 1.0
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//自定义AuthenticationManager
//自定义数据源,使用@Bean注入,原来默认的失效
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(
User.withUsername("root").password("{noop}123456").roles("admin").build());
return inMemoryUserDetailsManager;
}
//覆盖默认的AuthenticationManager
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
//从工厂中暴露出来
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//构造器注入,把自定义Filter交给工厂,用以替换UsernamePasswordAuthenticationFilter
//使用构造器注入,可以在构造方法中完成对filter的设置
@Bean
public LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setFilterProcessesUrl("/doLogin");//指定认证url
//自定义接收参数名
loginFilter.setUsernameParameter("uname");//指定接收json用户名key
loginFilter.setPasswordParameter("pwd");//指定接收json密码key
loginFilter.setAuthenticationManager(authenticationManagerBean());
//认证成功处理
loginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> result = new HashMap<String, Object>();
result.put("msg", "登录成功");//打印登录成功信息
//result.put("status", 200);//打印状态码 此处改为setStatus
result.put("用户信息", authentication.getPrincipal());//获得身份信息
result.put("authentication", authentication);//打印认证信息
response.setContentType("application/json;charset=UTF-8");//设置响应类型
response.setStatus(HttpStatus.OK.value());//设置登录成功之后的状态
String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串
response.getWriter().println(s);//打印json格式数据
}
});
//认证失败处理
loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
Map<String, Object> result = new HashMap<String, Object>();
result.put("msg", "登录失败:" + exception.getMessage());
//result.put("status", 500);//打印状态码 此处改为setStatus
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());//设置登录失败之后的状态
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
});
return loginFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()//所有请求必须认证
.and()
.formLogin()//form表单认证时默认采用UsernamePasswordAuthenticationFilter进行拦截
// 此处使用LoginFilter进行替换
.and()
.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().println("请认证之后再去处理!");
}
})
.and()
.logout()
//.logoutUrl("/logout")
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/logout", HttpMethod.DELETE.name()),
new AntPathRequestMatcher("/logout", HttpMethod.GET.name())
))
.logoutSuccessHandler((request, response, authentication) -> {
Map<String, Object> result = new HashMap<String, Object>();
result.put("msg", "注销成功");//打印登录成功信息
//result.put("status", 200);//打印状态码 此处改为setStatus
result.put("用户信息", authentication.getPrincipal());//获得身份信息
result.put("authentication", authentication);//打印认证信息
response.setContentType("application/json;charset=UTF-8");//设置响应类型
response.setStatus(HttpStatus.OK.value());//设置登录成功之后的状态
String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串
response.getWriter().println(s);//打印json格式数据
})
.and()
.csrf().disable();
/**
* addFilterAt:用某个filter替换掉过滤器链中的某个filter
* addFilterBefore():将某个过滤器放在过滤器链中某个filter之前
* addFilterAfter():将某个过滤器放在过滤器链中某个filter之后
*/
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
8.11 启动服务测试
启动服务,打开Postman进行测试:
-
(1)GET http://localhost:8080/test 登录失败
-
(2)POST http://localhost:8080/doLogin
uname:root,pwd:123
uname:blr,pwd:123
uname:admin,pwd:123
均可成功登录
-
(3)GET或DELETE http://localhost:8080/logout 成功注销