我自己的思路
先分别实现 userdetailsService,userDetails,passwordEncoder三个接口,
然后就是写登录逻辑 本文章用的是继承UsernamePasswordAuthenticationFilter这个接口 因为这个框架默认登录逻辑是在这里面的,里面的核心就是通过
这里这个接口可以不用写过滤器里,可以直接写在serviceImpl里面的登录逻辑里面,
关键是要AuthenticationManager()这个注入到spring容器里
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
AuthenticationManager().authenticate(authenticationToken);
登录逻辑写完后,需要写个解析token的过滤器,作用是看用户是否登录,访问其它接口的时候
所以它是在usernamepasswordFilter 之前的
然后把这个加入配置类,进行配置
1.认证过程
1.1、创建spring-security模块
在common模块下创建spring-security公共模块,创建方式如:service-util模块
1.2、添加依赖
修改pom.xml
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.atguigu</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-security</artifactId>
<dependencies>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>common-util</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Spring Security依赖 -->
<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>
<scope>provided </scope>
</dependency>
</dependencies>
</project>
说明:依赖包(spring-boot-starter-security)导入后,Spring Security就默认提供了许多功能将整个应用给保护了起来:
1、要求经过身份验证的用户才能与应用程序进行交互
2、创建好了默认登录表单
3、生成用户名为user
的随机密码并打印在控制台上
1.3、添加配置类
java
package com.atguigu.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
2、用户认证
用户认证流程:
2.1、用户认证核心组件
我们需要通过 SecurityContext
来获取Authentication
,SecurityContext
就是我们的上下文对象!这个上下文对象则是交由 SecurityContextHolder
进行管理,你可以在程序任何地方使用它:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
现在我们已经知道了Spring Security中三个核心组件:
1、Authentication
:存储了认证信息,代表当前登录用户
2、SeucirtyContext
:上下文对象,用来获取Authentication
3、SecurityContextHolder
:上下文管理对象,用来在程序任何地方获取SecurityContext
Authentication
中是什么信息呢:
1、Principal
:用户信息,没有认证时一般是用户名,认证后一般是用户对象
2、Credentials
:用户凭证,一般是密码
3、Authorities
:用户权限
2.2:代码实现思路:
首先说前三步是对这个地方做更改,因为它的默认行为,不符合开发要求
第一步:
实现UserDetailsService这个接口 里面有loadUserByUsername(String username)这个方法,为什么要重写,因为默认机制是在内存中查,而我们要在数据库中查
java
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
if(sysUser==null) throw new GuiguException(ResultCodeEnum.DATA_ERROR);
if(sysUser.getStatus()==0) throw new GuiguException(ResultCodeEnum.SERVICE_ERROR);
List<String> userPermsList = sysMenuService.findUserPermsByUserId(sysUser.getId());
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String perm : userPermsList) {
authorities.add(new SimpleGrantedAuthority(perm.trim()));
}
return new CustomUser(sysUser, authorities);
}
第二步
实现UserDetails这个接口 ,按需要重写里面的方法,因为它的属性不一定符合我们的要求
第三步
重写PasswordEncoder这个类,它的功能是对密码进行加密和校验,我用的是MD5进行加密
MD5工具类
java
public final class MD5 {
public static String encrypt(String strSrc) {
try {
char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f' };
byte[] bytes = strSrc.getBytes();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
bytes = md.digest();
int j = bytes.length;
char[] chars = new char[j * 2];
int k = 0;
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
chars[k++] = hexChars[b >>> 4 & 0xf];
chars[k++] = hexChars[b & 0xf];
}
return new String(chars);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException("MD5加密出错!!+" + e);
}
}
}
java
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()));
}
}
第四步
对这个部分进行修改,因为我们要让账号密码走我们自己写的登录controller
继承UsernamePasswordAuthenticationFilter 这个类,重写里面的登录逻辑,和认证成功,认证失败方法
java
import com.atguigu.common.jwt.JwtHelper;
import com.atguigu.common.result.Result;
import com.atguigu.common.result.ResultCodeEnum;
import com.atguigu.common.util.ResponseUtil;
import com.atguigu.security.custom.CustomUser;
import com.atguigu.vo.system.LoginVo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
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 org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
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;
/**
* <p>
* 登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验
* </p>
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
public TokenLoginFilter(AuthenticationManager authenticationManager) {
this.setAuthenticationManager(authenticationManager);
this.setPostOnly(false);
//指定登录接口及提交方式,可以指定任意路径
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
}
/**
* 登录认证
* @param req
* @param res
* @return
* @throws AuthenticationException
*/
@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);
}
}
/**
* 登录成功
* @param request
* @param response
* @param chain
* @param auth
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth) throws IOException, ServletException {
CustomUser customUser = (CustomUser) auth.getPrincipal();
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));
}
/**
* 登录失败
* @param request
* @param response
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
if(e.getCause() instanceof RuntimeException) {
ResponseUtil.out(response, Result.build(null, 204, e.getMessage()));
} else {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
}
}
}
第五步 写一个过滤器 这个过滤器的目的是访问其它接口(不包含登录接口)看是否有权限登录
java
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
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)) {
String authoritiesString = (String) redisTemplate.opsForValue().get(username);
List<Map> mapList = JSON.parseArray(authoritiesString, Map.class);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Map map : mapList) {
authorities.add(new SimpleGrantedAuthority((String)map.get("authority")));
}
return new UsernamePasswordAuthenticationToken(username, null, authorities);
} else {
return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
}
}
return null;
}
}
最后 配置用户认证
java
import com.atguigu.custom.CustomMd5PasswordEncoder;
import com.atguigu.fillter.TokenAuthenticationFilter;
import com.atguigu.fillter.TokenLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解功能,默认禁用注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Resource
private CustomMd5PasswordEncoder customMd5PasswordEncoder;
@Resource
private RedisTemplate redisTemplate;
@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(redisTemplate), UsernamePasswordAuthenticationFilter.class)
.addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate));
//禁用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");
}
}