SpringSecurity
springboot整合springsecurity
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
UserServiceImpl
数据库读取用户信息进行身份认证,实现UserDetailService接口重写loadUserByUsername方法
@Service("userService")
public class UserServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
/**
* 需新建配置类注册一个指定的加密方式Bean,或在下一步Security配置类中注册指定
*/
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 通过用户名从数据库获取用户信息
User user = userService.getUser(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 添加用户拥有的多个角色
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
Set<Role> roles = user.getRoles();
for (Role role : roles) {
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRole()));
}
return new User(
user.getUsername(),
//数据库存放密码应当加密,故此处直接使用
user.getPassword(),
//passwordEncoder.encode(user.getPassword()),
authorities
);
}
}
SecurityConfig
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级安全验证
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
/**
* 指定加密方式 BCrypt
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// 认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
// 从数据库读取的用户进行身份认证
.userDetailsService(userService)
.passwordEncoder(passwordEncoder());
}
// 授权
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/add-user").permitAll() // 允许post请求/add-user,而无需认证
.anyRequest().authenticated()// 所有请求都需要验证
.and()
.formLogin().loginPage("/toLogin")// 设置登录页及其参数
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login")
.and()
.logout().logoutSuccessUrl("/")// 注销
.and()
.rememberMe().rememberMeParameter("remember")// 记住我
.and()
.csrf().disable();// post请求要关闭csrf验证,不然访问报错;实际开发中开启,需要前端配合传递其他参数
}
}
controller
开启方法的权限注解后,配合@PreAuthorize(),限定方法访问权限
@PreAuthorize("hasAnyRole('user')")
public int updatePwd(String oldPwd, String newPwd) {
// 获取当前登录用户信息(注意:没有密码的)
UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = principal.getUsername();
// 通过用户名获取到用户信息(获取密码)
UserInfo userInfo = userInfoMapper.getUserInfoByUsername(username);
// 判断输入的旧密码是正确
if (passwordEncoder.matches(oldPwd, userInfo.getPassword())) {
// 不要忘记加密新密码
return userInfoMapper.updatePwd(username, passwordEncoder.encode(newPwd));
}
return 0;
}
springboot整合springsecurity+jwt
使用jwt的同时还使用了rea加密算法
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
yaml
rsa:
key:
pubKeyPath: C:\Users\robod\Desktop\auth_key\id_key_rsa.pub
priKeyPath: C:\Users\robod\Desktop\auth_key\id_key_rsa
RsaKeyProperties
@Data
@ConfigurationProperties("rsa.key") //指定配置文件的key
public class RsaKeyProperties {
private String pubKeyPath;
private String priKeyPath;
private PublicKey publicKey;
private PrivateKey privateKey;
@PostConstruct
public void createKey() throws Exception {
this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
}
}
UserServiceImpl
这里只实现简单的查找用户功能,把认证全部转移到了我们自定义的过滤器中
@Service("userService")
public class UserServiceImpl implements UserDetailsService{
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userMapper.findByUsername(username);
return sysUser;
}
}
RsaUtils
public class RsaUtils {
private static final int DEFAULT_KEY_SIZE = 2048;
/**
* 从文件中读取公钥
*
* @param filename 公钥保存路径,相对于classpath
* @return 公钥对象
* @throws Exception
*/
public static PublicKey getPublicKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPublicKey(bytes);
}
/**
* 从文件中读取密钥
*
* @param filename 私钥保存路径,相对于classpath
* @return 私钥对象
* @throws Exception
*/
public static PrivateKey getPrivateKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPrivateKey(bytes);
}
/**
* 获取公钥
*
* @param bytes 公钥的字节形式
* @return
* @throws Exception
*/
private static PublicKey getPublicKey(byte[] bytes) throws Exception {
bytes = Base64.getDecoder().decode(bytes);
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
/**
* 获取密钥
*
* @param bytes 私钥的字节形式
* @return
* @throws Exception
*/
private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
bytes = Base64.getDecoder().decode(bytes);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}
/**
* 根据密文,生存rsa公钥和私钥,并写入指定文件
*
* @param publicKeyFilename 公钥文件路径
* @param privateKeyFilename 私钥文件路径
* @param secret 生成密钥的密文
*/
public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 获取公钥并写出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
writeFile(publicKeyFilename, publicKeyBytes);
// 获取私钥并写出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
writeFile(privateKeyFilename, privateKeyBytes);
}
private static byte[] readFile(String fileName) throws Exception {
return Files.readAllBytes(new File(fileName).toPath());
}
private static void writeFile(String destPath, byte[] bytes) throws IOException {
File dest = new File(destPath);
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);
}
}
JwtUtils
使用了jwt框架jjwt
public class JwtUtils {
private static final String JWT_PAYLOAD_USER_KEY = "user";
/**
* 私钥加密token
*
* @param userInfo 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位分钟
* @return JWT
*/
public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey, int expire) {
return Jwts.builder()
.claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
.setId(createJTI())
.setExpiration(DateTime.now().plusMinutes(expire).toDate())
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
}
/**
* 私钥加密token
*
* @param userInfo 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位秒
* @return JWT
*/
public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey, int expire) {
return Jwts.builder()
.claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
.setId(createJTI())
.setExpiration(DateTime.now().plusSeconds(expire).toDate())
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
}
/**
* 公钥解析token
*
* @param token 用户请求中的token
* @param publicKey 公钥
* @return Jws<Claims>
*/
private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
}
private static String createJTI() {
return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
}
/**
* 获取token中的用户信息
*
* @param token 用户请求中的令牌
* @param publicKey 公钥
* @return 用户信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T> userType) {
Jws<Claims> claimsJws = parserToken(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());
claims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));
claims.setExpiration(body.getExpiration());
return claims;
}
/**
* 获取token中的载荷信息
*
* @param token 用户请求中的令牌
* @param publicKey 公钥
* @return 用户信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {
Jws<Claims> claimsJws = parserToken(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());
claims.setExpiration(body.getExpiration());
return claims;
}
}
SysUser
@Data
public class SysUser implements UserDetails {
private Integer id;
private String username;
private String password;
private Integer status;
private List<SysRole> roles = new ArrayList<>(); //SysRole封装了角色信息,和登录无关,我放在后面讲
//这里还有几个UserDetails中的方法,我就不贴代码了
}
SysRole
@Data
public class SysRole implements GrantedAuthority {
private Integer id;
private String roleName;
private String roleDesc;
/**
* 如果授予的权限可以当作一个String的话,就可以返回一个String
* @return
*/
@JsonIgnore
@Override
public String getAuthority() {
return roleName;
}
}
JwtLoginFilter
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RsaKeyProperties rsaKeyProperties;
public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) {
this.authenticationManager = authenticationManager;
this.rsaKeyProperties = rsaKeyProperties;
}
//这个方法是用来去尝试验证用户的
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
SysUser user = JSONObject.parseObject(request.getInputStream(),SysUser.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword())
);
} catch (Exception e) {
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("code", HttpServletResponse.SC_UNAUTHORIZED);
map.put("message", "账号或密码错误!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
throw new RuntimeException(e);
}
}
//成功之后执行的方法
@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,rsaKeyProperties.getPrivateKey(),24*60);
response.addHeader("Authorization", "RobodToken " + token); //将Token信息返回给用户
try {
//登录成功时,返回json格式进行提示
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
Map<String, Object> map = new HashMap<String, Object>(4);
map.put("code", HttpServletResponse.SC_OK);
map.put("message", "登陆成功!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
JwtVerifyFilter
public class JwtVerifyFilter extends BasicAuthenticationFilter {
private RsaKeyProperties rsaKeyProperties;
public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) {
super(authenticationManager);
this.rsaKeyProperties = rsaKeyProperties;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader("Authorization");
//没有登录
if (header == null || !header.startsWith("RobodToken ")) {
chain.doFilter(request, response);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
Map<String, Object> map = new HashMap<String, Object>(4);
map.put("code", HttpServletResponse.SC_FORBIDDEN);
map.put("message", "请登录!");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
return;
}
//登录之后从token中获取用户信息
String token = header.replace("RobodToken ","");
SysUser sysUser = JwtUtils.getInfoFromToken(token, rsaKeyProperties.getPublicKey(), SysUser.class).getUserInfo();
if (sysUser != null) {
Authentication authResult = new UsernamePasswordAuthenticationToken
(sysUser.getUsername(),null,sysUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}
}
SecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)// 开启权限控制的注解支持
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
//授权
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //关闭csrf
.authorizeRequests()
.antMatchers("/**").hasAnyRole("USER") //角色信息
.anyRequest() //其它资源
.authenticated() //表示其它资源认证通过后
.and()
.addFilter(new JwtLoginFilter(super.authenticationManager(),rsaKeyProperties))
.addFilter(new JwtVerifyFilter(super.authenticationManager(),rsaKeyProperties))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //禁用session
}
}
controller
开启权限注解后,配合@Secured,限定方法访问权限
@Secured("ROLE_PRODUCT")
@RequestMapping("/findAll")
public String findAll() {
return "产品列表查询成功";
}
SpringBootApplication
加入res配置
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class) //将配置类放入Spring容器中
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceApplication.class, args);
}
}
@EnableGlobalMethodSecurity
该注解有(jsr250Enabled = true, prePostEnabled = true, securedEnabled = true)三种机制,结合实际情况使用
springboot整合springsecurity+jwt(common-security)
具体应用参考官方文档
总结
-
认证
认证过程:UsernamePasswordAuthenticationFilter->SecurityConfig.configure(AuthenticationManagerBuilder auth)->UserDetailsService.loadUserByUsername()->UsernamePasswordAuthenticationFilter
要想实现认证,我们可以自由的替换后三个地方,实现我们想要的功能。即直接写在security配置中、重写UserDetailsService.loadUserByUsername()方法和实现UsernamePasswordAuthenticationFilter来实现。
- 认证用户实现UserDetails接口
- UserService实现UserDetailsService接口,重写loadUserByUsername()方法,从数据库中获取数据
- 实现自己的认证过滤器继承UsernamePasswordAuthenticationFilter,重写attemptAuthentication()和successfulAuthentication()方法,实现业务逻辑
- Spring Security的配置类继承自WebSecurityConfigurerAdapter,重写里面的两个config()方法
-
鉴权
- 封装权限信息的类实现GrantedAuthority接口,并实现里面的getAuthority()方法
- 实现自己的Token校验过滤器继承BasicAuthenticationFilter,并重写doFilterInternal()方法,实现业务逻辑
- 编写Spring Security的配置类继承WebSecurityConfigurerAdapter,重写configure()方法添加自定义的过滤器,
- 添加@EnableGlobalMethodSecurity(securedEnabled = true)注解开启注解权限控制的功能
参考链接
SpringBoot整合SpringSecurity(通俗易懂)
SpringBoot整合Spring Security【超详细教程】