【记录笔记,大佬勿喷】 关于SpringSecurity的登录流程,结合尚硅谷以及若依的项目分析
Spring Security基于Spring框架,提供了一套Web应用安全性的完整解决方案。Web应用的安全性包括用户认证(Authentication) 和 用户授权(Authonzation),这两点也是Spring Security重要核心功能。用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码来完成认证
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
SpringSecurity概述
Spring Security基于Spring框架,提供了一套Web应用安全性的完整解决方案。
Web应用的安全性包括用户认证(Authentication) 和 用户授权(Authonzation),这两点也是Spring Security重要核心功能。
用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录。
用户授权:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点说就是系统判断用户是否有权限去做某些事情。
简单来说 Spring Security 本质是一个过滤器链。Spring Security在启动时会加入很多的过滤器。
提示:以下是本篇文章正文内容,下面案例可供参考
一、SpringSecurity登录流程
二、入门案例
1.引入依赖
代码如下(示例):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.添加配置类
代码如下(示例):
```java
package com.jerry.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
/**
* ClassName: WebSecurityConfig
* Package: com.jerry.security.config
* Description:
*
* @Author jerry_jy
* @Create 2023-03-03 13:44
* @Version 1.0
*/
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig {
}
3. 编写测试的控制器以及查看结果
@RestController
public class TestController {
@GetMapping("/add")
public String add() {
return "hello security";
}
}
启动该项目,在启动时会在控制台打印一句话:
Using generated security password:
9042005d-094d-45a1-87ef-a840378f5e76
此时直接在浏览器访问:http://localhost:8800/add ,并不会直接返回预期的字符串,而是跳转到了一个登录页面。
spring security登录页面的默认用户名为 user,密码为控制台打印出来的那句UUID。
登录之后即可看到 hello security。
三、尚硅谷篇
正式使用其实有多种方法,总体流程是不会变的,结合自身情况选择最合适的即可。这里演示一个尚硅谷的和一个若依的。
1、编写自定义组件以及用户数据的准备
UserDetailsService接口,UserDetails主要是封装了一些用户数据,如果默认封装的不够用可以自己拓展直接继承User类,User类默认也是实现了UserDetailsService接口。
以下是自己拓展UserDetails:
public class CustomUser extends User {
/**
* 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象。(这里我就不写get/set方法了)
*/
private SysUser sysUser;
public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
super(sysUser.getUsername(), sysUser.getPassword(), authorities);
this.sysUser = sysUser;
}
public SysUser getSysUser() {
return sysUser;
}
public void setSysUser(SysUser sysUser) {
this.sysUser = sysUser;
}
}
public interface UserDetailsService {
/**
* 根据用户名获取用户对象(获取不到直接抛异常)
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
实现类:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询
SysUser sysUser = sysUserService.getUserByUserName(username);
if(null == sysUser) {
throw new UsernameNotFoundException("用户名不存在!");
}
if(sysUser.getStatus().intValue() == 0) {
throw new RuntimeException("账号已停用");
}
return new CustomUser(sysUser, Collections.emptyList());
}
}
SysUserService
SysUser getUserByUserName(String username);
SysUserServiceImpl
// 根据用户名查询
@Override
public SysUser getUserByUserName(String username) {
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getUsername,username);
SysUser sysUser = baseMapper.selectOne(queryWrapper);
return sysUser;
}
自定义密码校验器
@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}
2、自定义用户认证过滤器
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
// 构造方法
public TokenLoginFilter(AuthenticationManager authenticationManager){
this.setAuthenticationManager(authenticationManager);
this.setPostOnly(false);
//指定登录接口及提交方式,可以指定任意路径
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
}
// 登录认证过程
// 获取输入的用户名和密码,调用方法认证
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
// 获取用户信息
LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);
//封装对象
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
//调用方法
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 认证成功调用的方法
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth) throws IOException, ServletException {
// 获取当前用户
CustomUser customUser = (CustomUser) auth.getPrincipal();
// 生成token
String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
// 返回
Map<String, Object> map = new HashMap<>();
map.put("token", token);
ResponseUtil.out(response, Result.ok(map));
}
// 认证失败调用的方法
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
if(e.getCause() instanceof RuntimeException) {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.DATA_ERROR));
} else {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_AUTH));
}
}
}
认证过程主要是通过用户名和密码封装成Authentication对象,并使用AuthenticationManager里的authenticate方法进行用户名和密码的认证
//封装对象
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
调用authenticate方法进行用户名和密码的认证
this.getAuthenticationManager().authenticate(authenticationToken);
authenticate验证原理请参考:
authenticate验证账号密码过程
认证成功后进入成功的方法,获取当前用户,再根据当前用户生成一个token并返回。
// 认证成功调用的方法
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth) throws IOException, ServletException {
// 获取当前用户
CustomUser customUser = (CustomUser) auth.getPrincipal();
// 生成token
String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
// 返回
Map<String, Object> map = new HashMap<>();
map.put("token", token);
ResponseUtil.out(response, Result.ok(map));
}
认证失败就不说了直接返回失败信息即可。
3、自定义Toekn验证过滤器
public class TokenAuthenticationFilter extends OncePerRequestFilter {
public TokenAuthenticationFilter() {
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
logger.info("uri:"+request.getRequestURI());
//如果是登录接口,直接放行
if("/admin/system/index/login".equals(request.getRequestURI())) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if(null != authentication) {
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} else {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// token置于header里
String token = request.getHeader("token");
logger.info("token:"+token);
if (!StringUtils.isEmpty(token)) {
String username = JwtHelper.getUsername(token);
logger.info("username:"+username);
if (!StringUtils.isEmpty(username)) {
return new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList());
}
}
return null;
}
}
主要思路就是先获取头部cookie里的token,先判断是否为空,不为空在使用jwt工具解析当前token获取当前用户并封装成UsernamePasswordAuthenticationToken对象,再交给SpringSecurity上下文。
4、SpringSecurity的配置信息
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService; // 装载的是 org.springframework.security.core.userdetails.UserDetailsService;
@Autowired
private CustomMd5PasswordEncoder customMd5PasswordEncoder;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
http
//关闭csrf跨站请求伪造
.csrf().disable()
// 开启跨域以便前端调用接口
.cors().and()
.authorizeRequests()
// 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
.antMatchers("/admin/system/index/login").permitAll()
// 这里意思是其它所有接口需要认证才能访问
.anyRequest().authenticated()
.and()
//TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
.addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilter(new TokenLoginFilter(authenticationManager()));
//禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定UserDetailService和加密器
auth.userDetailsService(userDetailsService)
.passwordEncoder(customMd5PasswordEncoder);
}
/**
* 配置哪些请求不拦截
* 排除swagger相关请求
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/favicon.ico", "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
}
}
5、用户权限控制
权限控制的主要思路就是在UserDetailsServiceImpl中重loadUserByUsername时,不仅通过用户名要查询到用户信息,还要将权限信息一块查询出来并返回。并在登录认证的过滤器当中(TokenLoginFilter),将权限信息拿出来放到Redis里(key为当前用户,value为查出来的权限信息)。在Token校验过滤器中通过解析token得到当前用户,并通过当前用户从redis里拿取当前权限信息
修改UserDetailsServiceImpl中的loadUserByUsername
// 根据 user_id 查询用户操作权限数据
List<String> userPermsList = sysMenuService.findUserPermsByUserId(sysUser.getId());
// 创建list集合,封装最终权限数据
List<SimpleGrantedAuthority> authList = new ArrayList<>();
// 遍历 authList
for (String perms : userPermsList) {
authList.add(new SimpleGrantedAuthority(perms.trim()));
}
return new CustomUser(sysUser, authList);
修改TokenLoginFilter(登录认证过滤器)
修改TokenAuthenticationFilter(Token校验过滤器)
修改WebSecurityConfig
配置类添加注解:
Spring Security默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,来判断用户对某个控制层的方法是否具有访问权限。
开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认
6、Controller层权限注解的应用
@PreAuthorize("hasAuthority('bnt.sysRole.list')")
controller层中加上这个注解即可判断该接口是否有权限访问,bnt.sysRole.list这些权限字符串都是通过数据库查出来的,再重写loadByUserName中就已经查出并返回了。
7、总结
1、先准备用户数据,使用userDeatils封装好用户数据,觉得不够用的话直接继承User自己拓展。
2、实现UserDetailsService接口并重写 loadUserByUsername方法,通过用户名查询用户信息,然后实现基本的用户信息校验逻辑,权限信息也一起查出来并返回。
3、准备登录认证过滤器或者直接从登录控制器中自己写登录逻辑,登录逻辑主要是认证账号密码,通过UsernamePasswordAuthenticationToken传入账号密码封装成 Authentication对象,并调用AuthenticationManager里的authenticate方法实现账号密码的认证。如果认证成功再通过Authentication对象的getPrincipal()方法获取当前用户信息,在使用redis将用户名当做key,根据当前用户信息获得的权限字符串当做Value存起来。最后使用jwt工具根据用户名信息生成token返回给前端。
4、token的验证主要是靠Token校验器,通过获取头部的cookie信息得到token并用jwt解析token得到当前用户信息,得到用户名在通过用户名从redis里拿到权限信息
5、最后就是配置springSecurity信息了,WebSecurityConfig 。
更多推荐
所有评论(0)