Spring Security-整合Spring Boot(分布
前置文章:
Spring Security-整合Spring Boot(分布式)-篇章一:主要介绍了JWT、RSA以及Security的认证校验逻辑;
Spring Security-整合Spring Boot(分布式)-篇章二:主要介绍JWT工具类,以及RSA工具类的基础使用。是一些固定的用法,可以跟据需求了解即可;
前言:本文是认证模块和资源模块的代码实现,其中【用户认证】、【身份校验】两个过滤器是关键内容。
零、本文纲要
一、security_auth_server(认证模块)
二、security_source_product(资源模块)
一、security_auth_server(认证模块)
- application.yml
- domain/entity
- mapper
- service接口及实现类
- filter
- config
- 启动类
0. application.yml
server:
port: 9001
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///security_authority?serverTimezone=UTC&useSSL=false
username: root
password: root
mybatis:
type-aliases-package: com.stone.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.stone: debug
rsa:
key:
publicKeyFile: D:\tmp\auth\id_key_rsa.pub
privateKeyFile: D:\tmp\auth\id_key_rsa.pri
1. domain/entity
- ① SysRole
GrantedAuthority是认证业务授权的接口
public class SysRole implements GrantedAuthority {
private Integer id;
private String roleName;
private String roleDesc;
/*
此处省略对象属性的get/set方法
*/
@JsonIgnore //此方法是规范的方法,不做序列化处理
@Override
public String getAuthority() {
return roleName;
}
}
- ② SysUser
UserDetails是认证业务的返回值
public class SysUser implements UserDetails {
private Integer id;
private String username;
private String password;
private Integer status;
private List<SysRole> roles;
/*
此处省略对象属性的get/set方法
*/
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true; //账户是否失效
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true; //账户是否锁定
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true; //凭据(密码)是否失效
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true; //是否可用
}
}
注意:此处我们把账户状态的内容写死了,具体业务如果需要灵活的状态,需要手动编写其逻辑代码。
2. mapper
- ① RoleMapper
public interface RoleMapper{
@Select("select r.id, r.role_name roleName, r.role_desc roleDesc " +
"from sys_role r, sys_user_role ur " +
"where r.id = ur.rid and ur.uid = #{uid}")
public List<SysRole> findById(Integer uid);
}
- ② UserMapper
public interface UserMapper{
@Select("select * from sys_user where username = #{username}")
@Results({
@Result(id = true, property = "id", column = "id"),
@Result(property = "roles", column = "id", javaType = List.class,
many = @Many(select = "com.stone.mapper.RoleMapper.findById"))
})
public SysUser findByName(String username);
}
3. service接口及实现类
- ① UserService
public interface UserService extends UserDetailsService {
}
- ② UserServiceImpl
此处我们重写UserDetailsService接口的loadUserByUsername方法,此方法是认证业务需要使用到的。另外,因为我们的SysRole和SysUser分别实现了GrantedAuthority和UserDetails接口,所以调用mapper获取的结果即是框架所需的UserDetails实现类。
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userMapper.findByName(username);
}
}
4. filter
- ① JwtLoginFilter
此处我们继承UsernamePasswordAuthenticationFilter类,重写其attemptAuthentication认证用户方法,以及认证通过回写token的方法successfulAuthentication。
Ⅰ attemptAuthentication认证用户方法
原方法是接收POST请求的form表单数据。而前后端分离的开发环境下,我们更多使用AJAX请求,传递的是json字符串。所以此处做响应的代码调整。
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RsaKeyProperties prop;
public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
this.authenticationManager = authenticationManager;
this.prop = prop;
}
/*
* 认证用户的方法
* @param request 请求对象
* @param response 响应对象
* @return Authentication
* @throws AuthenticationException 认证异常
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
//从请求对象中获取SysUser对象
SysUser sysUser = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);
//封装为UsernamePasswordAuthenticationToken用于用户认证
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
sysUser.getUsername(),
sysUser.getPassword());
//进行具体的认证
return authenticationManager.authenticate(authRequest);
} catch (Exception e) {
try {
//如果认证失败,提供自定义json格式异常
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map<String, Object> map = new HashMap<String, Object>();
map.put("code", HttpServletResponse.SC_UNAUTHORIZED);
map.put("message", "账号或密码错误!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
} catch (Exception ex) {
ex.printStackTrace();
}
throw new RuntimeException(e);
}
}
/*
* 认证通过回写token的方法
* @param request 请求对象
* @param response 响应对象
* @param chain 过滤器链
* @param authResult 认证结果
* @throws IOException IO异常
* @throws ServletException Servlet异常
*/
@Override
public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SysUser sysUser = new SysUser();
sysUser.setUsername(authResult.getName());
sysUser.setRoles((List<SysRole>) authResult.getAuthorities());
String token = JwtUtils.generateTokenExpireInMinutes(sysUser, prop.getPrivateKey(), 24 * 60);
/*
此处引用了JWT官方对token的规范说明,具体如下:
https://jwt.io/introduction/
Whenever the user wants to access a protected route or resource, the user agent should send the JWT,
typically in the Authorization header using the Bearer schema. The content of the header should look
like the following:
Authorization: Bearer <token>
*/
//往响应头中添加token
response.addHeader("Authorization", "Bearer " + token);
try {
//登录成功時,返回json格式进行提示
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
Map<String, Object> resultMap = new HashMap<String, Object>();
resultMap.put("code", HttpServletResponse.SC_OK);
resultMap.put("message", "认证通过!");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
- ② JwtVerifyFilter
此处我们自定义JwtVerifyFilter继承token校验的filter类BasicAuthenticationFilter
public class JwtVerifyFilter extends BasicAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RsaKeyProperties rsaKeyProperties;
public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) {
super(authenticationManager);
this.rsaKeyProperties = rsaKeyProperties;
}
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//从请求头中获取token
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
//如果没有携带错误的token(未有效认证的),则提示用户登录!
chain.doFilter(request, response);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
Map<String, Object> resultMap = new HashMap<String, Object>();
resultMap.put("code", HttpServletResponse.SC_FORBIDDEN);
resultMap.put("msg", "请登录!");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
return;
}
//如果携带了正确格式的token,首先得到token
String token = header.replace("Bearer ", "");
//公钥解密,验证token是否正确
Payload<SysUser> payload = JwtUtils.getInfoFromToken(token, rsaKeyProperties.getPublicKey(), SysUser.class);
SysUser sysUser = payload.getUserInfo();
if (sysUser != null) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
sysUser.getUsername(), null, sysUser.getAuthorities());
//往SecurityContext容器中存储授权信息
SecurityContextHolder.getContext().setAuthentication(authRequest);
chain.doFilter(request, response);
}
}
}
5. config
- ① RsaKeyProperties
用于生成公钥私钥的配置类
@ConfigurationProperties("rsa.key")
public class RsaKeyProperties {
private String publicKeyFile;
private String privateKeyFile;
private PublicKey publicKey;
private PrivateKey privateKey;
@PostConstruct //在对象加载完依赖注入后执行(Constructor > @Autowired > @PostConstruct),生成公钥私钥
public void createRsaKey() throws Exception {
publicKey = RsaUtils.getPublicKey(publicKeyFile);
privateKey = RsaUtils.getPrivateKey(privateKeyFile);
}
/*
此处省略对象属性的get/set方法
*/
}
- ② WebSecurityConfig
@Configuration
@EnableWebSecurity //开启认证配置的支持,继承WebSecurityConfigurerAdapter重写配置
@EnableGlobalMethodSecurity(securedEnabled = true) //开启注解的支持
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private RsaKeyProperties rsaKeyProperties;
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//认证用户的来源【数据库】
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
//配置Spring Security的相关信息
@Override
public void configure(HttpSecurity http) throws Exception {
//释放静态资源,指定资源拦截规则,指定自定义认证页面,指定退出认证配置,csrf配置
http.cors().and().csrf().disable()
.authorizeRequests().antMatchers("/**").hasAnyRole("USER")
.anyRequest().authenticated()
.and()
.addFilter(new JwtLoginFilter(authenticationManager(), rsaKeyProperties))
.addFilter(new JwtVerifyFilter(authenticationManager(), rsaKeyProperties))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//不创建HttpSession
}
}
6. 启动类
@SpringBootApplication
@MapperScan("com.stone.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class) //开启@ConfigurationProperties注解的支持
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
}
}
7. 测试
postman
POST请求:http://localhost:9001/login
Body-raw-JSON
{
"username":"stone",
"password":"123456"
}
此时,我们可以在响应头中获取到认证后的token
二、security_source_product(资源模块)
- application.yml
- domain/entity
- mapper
- service接口及实现类
- filter
- config
- controller
- 启动类
0. application.yml
注意:此处除了端口不同之外,重要的是需要把私钥从配置文件中去除,只配置公钥信息
server:
port: 9002
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///security_authority?serverTimezone=UTC&useSSL=false
username: root
password: root
mybatis:
type-aliases-package: com.stone.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.stone: debug
rsa:
key:
publicKeyFile: D:\tmp\auth\id_key_rsa.pub
1. domain/entity
注意:此处的SysRole、SysUser与认证模块的一致,可以跟据项目实际需要,看是否把这些类提取到common共用模块
2. mapper
注意:由于SysRole、SysUser的关键信息是通过token解析出来的,所以此处不需要认证相关的mapper
3. service接口及实现类
- ① ProductService
public interface ProductService {
public String findAll();
}
- ② ProductServiceImpl
@Service
public class ProductServiceImpl implements ProductService {
@Secured({"ROLE_ADMIN","ROLE_PRODUCT"})
@Override
public String findAll() {
return "产品列表查询成功";
}
}
4. filter
- ① JwtVerifyFilter
该类与server模块的一致。
注意:用户认证的过滤在server模块进行,此处只需要配置JwtVerifyFilter类。
5. config
- ① RsaKeyProperties
注意:由于资源模块只需做身份校验,此处仅配置公钥的内容。
@ConfigurationProperties("rsa.key")
public class RsaKeyProperties {
private String publicKeyFile;
private PublicKey publicKey;
@PostConstruct
public void createRsaKey() throws Exception {
publicKey = RsaUtils.getPublicKey(publicKeyFile);
}
/*
此处省略对象属性的get/set方法
*/
}
- ② WebSecurityConfig
注意:与认证模块相比,此处的配置少了密码编码类,以及认证用户的来源的配置。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RsaKeyProperties rsaKeyProperties;
//配置Spring Security的相关信息
@Override
public void configure(HttpSecurity http) throws Exception {
//释放静态资源,指定资源拦截规则,指定自定义认证页面,指定退出认证配置,csrf配置
http.cors().and().csrf().disable()
.authorizeRequests().antMatchers("/product").hasAnyRole("PRODUCT")
.anyRequest().authenticated()
.and()
.addFilter(new JwtVerifyFilter(authenticationManager(), rsaKeyProperties))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
6. controller
- ① ProductController
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@RequestMapping("/findAll")
public String findAll(){
return productService.findAll();
}
}
7. 启动类
@SpringBootApplication
@MapperScan("com.stone.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class)
public class AuthSourceApplication {
public static void main(String[] args) {
SpringApplication.run(AuthSourceApplication.class, args);
}
}
8. 测试
postman
Headers中添加:
Key:Authorization
Value:Bearer [登录测试获取到的token]
此时我们就可以访问对应的资源
三、结尾
以上即为Spring Security-整合Spring Boot(分布式)的全部内容,感谢阅读。