主要引入了security、redis、jpa和jjwt的相关依赖。
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.4.RELEASEversion>
<relativePath/>
parent>
<groupId>com.securitygroupId>
<artifactId>demoartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>demoname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
application.yml
全局配置文件。
spring:
datasource:
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
jpa:
database: mysql
show-sql: true
hibernate:
ddl-auto: update
redis:
host: localhost
port: 6379
password:
database: 0 #Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0
timeout: 5000 #连接超时时间(ms)
jwt:
#签名
signingKey: fucker
#过期时间(单位:秒)
expiration: 600
采用RBAC模式建立数据库。RBAC 是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这样的方式管理层级相互依赖,权限赋予给角色,而把角色又赋予用户,用户量不大的时候管理起来很方便。
权限实体类。
package com.security.bean;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
@Data
public class Permission {
@Id @GeneratedValue
private long id;//主键.
private String url;//授权链
}
角色实体类。
package com.security.bean;
import lombok.Data;
import javax.persistence.*;
import java.util.List;
@Entity
@Data
public class Role {
@Id @GeneratedValue
private long rid;//主键.
private String name;//角色名称.
private String description;//角色描述.
// 角色 - 权限是多对多的关系
@ManyToMany(fetch= FetchType.EAGER)
@JoinTable(name="RolePermission",joinColumns= {@JoinColumn(name="role_id")} , inverseJoinColumns= {@JoinColumn(name="permission_id")})
private List<Permission> permissions;
}
用户实体类。
package com.security.bean;
import lombok.Data;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
@Entity
@Data
public class UserInfo {
@Id @GeneratedValue
private long uid;//主键.
private String username;//用户名.
private String password;//密码.
//用户--角色:多对多的关系.
@ManyToMany(fetch=FetchType.EAGER)//立即从数据库中进行加载数据;
@JoinTable(name = "UserRole", joinColumns = { @JoinColumn(name = "uid") }, inverseJoinColumns ={@JoinColumn(name = "role_id") })
private List<Role> roles;
}
用于查询数据库(使用jpa就是这么简单)。
package com.security.repository;
import com.security.bean.UserInfo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserInfoRepository extends JpaRepository<UserInfo, Long> {
UserInfo findByUsername(String username);
}
主要是对redis和jwt的操作,使用redis实现登录、登出动态管理。
package com.security.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class TokenServiceImpl {
public Integer expiration;
public String signingKey;
final StringRedisTemplate redisTemplate;
public TokenServiceImpl(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public String createToken(String username, Collection<? extends GrantedAuthority> authorities){
String jwtToken=Jwts.builder()
//存入用户权限信息
.claim("authorities", authorities)
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + expiration*1000))
.signWith(SignatureAlgorithm.HS512,signingKey)
.compact();
//将jwtToken存入redis
if(set(jwtToken,jwtToken)){
return jwtToken;
}else {
return null;
}
}
/**
* 获取存入jwt的个人变量authorities。
*/
public Collection<GrantedAuthority> getAuthority(Claims claims){
Collection<GrantedAuthority> authorities=new ArrayList<>();
String[] as=claims.get("authorities").toString().replace("[","").replace("]","").split(",");
for (String a:as){
authorities.add(new SimpleGrantedAuthority(a));
}
return authorities;
}
/**
* 解析redis中存的jwtToken
*/
public Claims parseToken(String jwtToken){
String token = get(jwtToken.replace("Bearer","").trim());
Claims claims =null;
try {
claims = Jwts.parser()
.setSigningKey(signingKey)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
e.printStackTrace();
}
return claims;
}
/**
* 下面的方法对redis进行操作,包括存、取和删。
*/
private boolean set(String key, String value) {
try {
redisTemplate.opsForValue().set(key, value,expiration, TimeUnit.SECONDS);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
private String get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
public boolean del(String key) {
try {
redisTemplate.delete(key.replace("Bearer","").trim());
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
匿名未登录访问的时执行。
package com.security.filter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 匿名未登录访问的时执行
*/
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
//设置response状态码,返回错误信息等
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(e.getMessage());
}
}
没有权限,被拒绝访问时执行。
package com.security.filter;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 没有权限,被拒绝访问时执行
*/
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
//设置response状态码,返回错误信息等
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(accessDeniedException.getMessage());
}
}
登录过滤,请求登录接口后进入attemptAuthentication
方法,登录成功执行successfulAuthentication
方法,登录失败执行unsuccessfulAuthentication
方法。
package com.security.filter;
import com.security.service.TokenServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
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.core.GrantedAuthority;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
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.io.PrintWriter;
import java.util.Collection;
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
@Autowired
TokenServiceImpl tokenService;
public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException {
System.err.println("__________________________________________________");
//从请求参数中获取用户名和密码
String username = req.getParameter("username");
String password = req.getParameter("password");
//该方法会去调用CustomUserDetailServiceImpl.loadUserByUsername
return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
/**
* 登录成功处理
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//获取用户角色和用户名
Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();
String username=authResult.getName();
//创建token
String jwt =tokenService.createToken(username,authorities);
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(jwt);
out.flush();
out.close();
}
/**
* 登录失败处理
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(failed.getMessage());
out.flush();
out.close();
}
}
请求过滤,主要进行token验证,然后将认证对象放入security上下文。
package com.security.filter;
import com.security.service.TokenServiceImpl;
import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
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.Collection;
@Component
public class JwtFilter extends OncePerRequestFilter {
private final TokenServiceImpl tokenService;
public JwtFilter(TokenServiceImpl tokenService) {
this.tokenService = tokenService;
}
/**
* 验证authorization
*/
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
System.err.println("==================================================");
String jwtToken = httpServletRequest.getHeader("authorization");
System.out.println(jwtToken);
//解析authorization
Claims claims = tokenService.parseToken(jwtToken);
if(claims!=null){
//获取当前登录用户名//获取用户角色
String username = claims.getSubject();
Collection<GrantedAuthority> authorities=tokenService.getAuthority(claims);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
退出成功后执行,删除redis中的token。
package com.security.filter;
import com.security.service.TokenServiceImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义退出处理类 返回成功
*/
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
private final TokenServiceImpl tokenService;
public CustomLogoutSuccessHandler(TokenServiceImpl tokenService) {
this.tokenService = tokenService;
}
/**
* 退出处理
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException{
String jwtToken = request.getHeader("authorization");
String username = tokenService.parseToken(jwtToken).getSubject();
tokenService.del(jwtToken);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(username + "退出成功。。。");
}
}
登录会自动调用此方法,将登录用户对象返回给security。
package com.security.service;
import com.security.bean.Permission;
import com.security.bean.Role;
import com.security.bean.UserInfo;
import com.security.repository.UserInfoRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.Component;
import java.util.ArrayList;
import java.util.Collection;
@Component
public class CustomUserDetailServiceImpl implements UserDetailsService{
private final UserInfoRepository userInfoRepository;
public CustomUserDetailServiceImpl(UserInfoRepository userInfoRepository) {
this.userInfoRepository = userInfoRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("++++++++++++++++++++++++++++++++++++++++++++++++++");
//通过username获取用户信息
UserInfo userInfo = userInfoRepository.findByUsername(username);
if(userInfo == null) {
throw new UsernameNotFoundException("not found");
}
//定义权限列表.
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(Role role:userInfo.getRoles()){
for (Permission perm:role.getPermissions()){
authorities.add(new SimpleGrantedAuthority(perm.getUrl()));
}
}
User userDetails = new User(username,userInfo.getPassword(),authorities);
System.out.println("当前用户拥有权限:"+userDetails.getAuthorities());
return userDetails;
}
}
在这里面进行权限验证(还可以补充更多的方法)。
package com.security.service;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
/**
* 权限验证类
*/
@Component
public class PermissionServiceImpl {
final TokenServiceImpl tokenService;
public PermissionServiceImpl(TokenServiceImpl tokenService) {
this.tokenService = tokenService;
}
/**
* 验证用户是否具备某权限
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public boolean hasPermission(String permission){
System.out.println("..................................................");
if (StringUtils.isEmpty(permission)){
return false;
}
//获取当前登录用户的jwtToken。
String jwtToken = getRequest().getHeader("authorization");
System.out.println(jwtToken);
//直接从jwt中解析用户权限,避免重复查询数据库。
Collection<GrantedAuthority> authorities=tokenService.getAuthority(tokenService.parseToken(jwtToken));
//封装当前用户拥有的权限
Collection<String> permissions=new ArrayList<>();
for(GrantedAuthority authority:authorities){
String perm=authority.getAuthority().split("=")[1].replace("}","");
permissions.add(perm);
}
System.out.println("当前需要权限:"+permission);
System.out.println("当前用户权限:"+permissions);
return permissions.contains(permission);
}
private HttpServletRequest getRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
}
主要配置了加密方式和过滤器。
package com.security.config;
import com.security.filter.*;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
//添加@EnableGlobalMethodSecurity注解开启Spring方法级安全
// prePostEnabled 决定Spring Security的前注解是否可用 [@PreAuthorize,@PostAuthorize,..],设置为true
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomLogoutSuccessHandler customLogoutSuccessHandler;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final JwtFilter jwtFilter;
public WebSecurityConfig(CustomLogoutSuccessHandler customLogoutSuccessHandler, CustomAccessDeniedHandler customAccessDeniedHandler, CustomAuthenticationEntryPoint customAuthenticationEntryPoint, JwtFilter jwtFilter) {
this.customLogoutSuccessHandler = customLogoutSuccessHandler;
this.customAccessDeniedHandler = customAccessDeniedHandler;
this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
this.jwtFilter = jwtFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
//登录后,访问没有权限处理类
.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler)
//匿名访问,没有权限的处理类
.authenticationEntryPoint(customAuthenticationEntryPoint)
//退出登录
.and()
.logout().logoutUrl("/logout").logoutSuccessHandler(customLogoutSuccessHandler)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.anyRequest().authenticated()
//无状态登录,取消session管理
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//登录请求过滤
.addFilterBefore(jwtLoginFilter(),UsernamePasswordAuthenticationFilter.class)
//验证authorization过滤
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public JwtLoginFilter jwtLoginFilter() throws Exception {
return new JwtLoginFilter("/login",authenticationManager());
}
/**
* 指定加密方式
*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
使用@PreAuthorize
注解实现权限验证。
这里参考了若依的前后端分离框架 http://www.ruoyi.vip/(官方网站,值得拥有)。
package com.security.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/user")
@PreAuthorize("@permissionServiceImpl.hasPermission('helloUser')")
public String hello() {
return "hello user !";
}
@GetMapping("/admin")
@PreAuthorize("@permissionServiceImpl.hasPermission('helloAdmin')")
public String admin() {
return "hello admin !";
}
}
启动项目,jpa会自动帮我们生成表。运行单元测试添加两个用户,之后手动添加了一些角色权限信息。
这样用户user就只拥有helloUser
权限,而用户admin拥有helloAdmin
和helloUser
权限。
用户信息表。
角色信息表。
用户角色关系表。
权限信息表。
角色权限关系表。
直接使用单元测试往数据库中添加两个用户(密码要加密)。
package com.security;
import com.security.bean.UserInfo;
import com.security.repository.UserInfoRepository;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = DemoApplication.class)
class DemoApplicationTests {
@Autowired
private UserInfoRepository userInfoRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Test
void contextLoads() {
UserInfo admin = new UserInfo();
admin.setUsername("admin");
admin.setPassword(passwordEncoder.encode("123456"));
userInfoRepository.save(admin);
UserInfo user = new UserInfo();
user.setUsername("user");
user.setPassword(passwordEncoder.encode("123456"));
userInfoRepository.save(user);
}
}
使用Postman进行测试。Postman官网 https://www.postman.com/(各种请求,应有尽有)。
未登录
登录user用户
登录使用Post
请求,用户名username
,密码password
。
user用户允许访问/user
接口(将登录接口返回的token放入Authorization
栏的Bearer Token
中)。
user用户禁止访问/admin
接口。
登录admin用户
admin用户允许访问/user
接口。
admin用户允许访问/admin
接口。
退出登录
使用GET
或者POST
请求http://localhost:8080/logout
,需要带上token。
最后附上代码资源链接 https://download.csdn.net/download/weixin_43424932/12195722。