初始化 spring boot 项目
添加 pom.xml 文件
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
@RestController
@RequestMapping("/demo")
public class DemoController {
@RequestMapping("/handle01")
public String handle01() {
return "你好 spring security";
}
}
访问 127.0.0.1:8080/demo/handle01 会进入表单页面。
默认 username=user;password 打印在控制台。
传输的用户名和密码使用 base64模式进行加密;是可逆的;不安全。
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests().anyRequest().authenticated();
}
}
默认的认证方式。UsernamePasswordAuthenticationFilter。默认的登录url : /login POST;参数名:username password 。
开启表单认证:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and().authorizeRequests()
.antMatchers("/demo/login", "/code/**").permitAll() // 不需要验证的接口
.anyRequest().authenticated();
}
}
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) {
// 排除静态资源被拦截
web.ignoring().antMatchers("/images/**");
}
}
操作实体采用 spring-data-jpa。
基本语法可参考:https://blog.csdn.net/qq_43439920/article/details/123165903?spm=1001.2014.3001.5501
开启后,会去数据库验证用户是否合法。控制台不会再打印密码。
@Service
public class PayingUserService implements UserDetailsService {
@Autowired
private PayingUserDao payingUserDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 基于数据库进行验证
PayingUser payingUser = new PayingUser();
payingUser.setUsername(username);
Example<PayingUser> payingUserExample = Example.of(payingUser);
Optional<PayingUser> payingUserOptional = payingUserDao.findOne(payingUserExample);
if (payingUserOptional.isEmpty()) {
// 用户名没有找到
System.out.println(username + "没有找到...");
throw new UsernameNotFoundException(username);
}
// 权限集合
Collection<GrantedAuthority> authorities = new ArrayList<>();
// {noop}:密码的加密方式; noop => 不加密
return new User(user.getUsername(), "{noop}" + user.getPassword(), authorities);
}
}
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private PayingUserService payingUserService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义用户认证
auth.userDetailsService(payingUserService);
}
}
// 推荐加密使用:bcrypt 强哈希方法 每次加密的结果都不一样 所以更安全
return new User(user.getUsername(), "{bcrypt}" + user.getPassword(), authorities);
@RequestMapping(value = "/save")
public PayingUser save(String username, String password) {
// bcrypt 加密
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encodePassword = bCryptPasswordEncoder.encode(password);
// 将加密后的数据存入数据库
PayingUser payingUser = new PayingUser();
payingUser.setUsername(username);
payingUser.setPassword(encodePassword);
payingUser.setStatus(1);
return payingUserDao.save(payingUser);
}
// 获取当前用户的三种方式
@RequestMapping("/getCurrentUser1")
public UserDetails getCurrentUser1() {
return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
@RequestMapping("/getCurrentUser2")
public UserDetails getCurrentUser2(Authentication authentication) {
return (UserDetails) authentication.getPrincipal();
}
@RequestMapping("/getCurrentUser3")
public UserDetails getCurrentUser3(@AuthenticationPrincipal UserDetails userDetails) {
return userDetails;
}
方便用户下一次登录的时候,不用再次输入用户名和密码登录。
// 记住我功能 默认token失效时间是两周 单位 S
.and().rememberMe().tokenValiditySeconds(60).rememberMeParameter("remember-me")
@Autowired
private DataSource dataSource;
@Bean
// 持久化 Token 支持
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 自动创建记录 token 的表 下次启动需要注释掉
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
.and().rememberMe().tokenValiditySeconds(60).rememberMeParameter("remember-me").tokenRepository(persistentTokenRepository())
# 默认创建的 token 表
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
// 判断当前操作是否来自 remember-me
@RequestMapping("/{id}")
public PayingUser getById(@PathVariable Integer id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass())) {
System.out.println("认证信息来源 remember-me,请重新登录");
throw new RememberMeAuthenticationException("认证信息来源 remember-me,请重新登录");
}
return payingUserDao.findById(id).get();
}
@Service
public class AuthenticationService implements AuthenticationSuccessHandler, AuthenticationFailureHandler, LogoutSuccessHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("登录失败的后续处理...");
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功的后续处理...");
}
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("退出登录的后续处理...");
}
}
@Autowired
private AuthenticationService authenticationService;
.successHandler(authenticationService)
.failureHandler(authenticationService)
生成图形验证码的接口需要不被拦截。
/**
* 处理生成验证码的请求
*/
@RestController
@RequestMapping("/code")
public class ValidateCodeController {
public final static String REDIS_KEY_IMAGE_CODE = "REDIS_KEY_IMAGE_CODE";
public final static int expireIn = 600; // 验证码有效时间 60s
@Autowired
public StringRedisTemplate stringRedisTemplate;
@RequestMapping("/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//获取访问IP
String remoteAddr = request.getRemoteAddr();
//生成验证码对象
ImageCode imageCode = createImageCode();
//生成的验证码对象存储到redis中 KEY为REDIS_KEY_IMAGE_CODE+IP地址
stringRedisTemplate.boundValueOps(REDIS_KEY_IMAGE_CODE + "-" + remoteAddr).set(imageCode.getCode(), expireIn, TimeUnit.SECONDS);
//通过IO流将生成的图片输出到登录页面上
ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
}
/*
用于生成验证码对象
*/
private ImageCode createImageCode() {
int width = 100; // 验证码图片宽度
int height = 36; // 验证码图片长度
int length = 4; // 验证码位数
//创建一个带缓冲区图像对象
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//获得在图像上绘图的Graphics对象
Graphics g = image.getGraphics();
Random random = new Random();
//设置颜色、并随机绘制直线
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
//生成随机数 并绘制
StringBuilder sRand = new StringBuilder();
for (int i = 0; i < length; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand.append(rand);
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, sRand.toString());
}
/*
获取随机演示
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
application.yml
server:
servlet:
session:
# 设置 session 的过期时间;默认 30 min;spring boot 最低 60 S
timeout: 60
// session 过期后的处理
http.sessionManagement()
// 无效后跳转页面
.invalidSessionUrl("/demo/login")
// 最大会话数量
.maximumSessions(1)
// 当达到最大会话数量的时候不允许登录
.maxSessionsPreventsLogin(true);
一个服务会至少部署在两台服务器。如果第一次用户访问的是服务器1,第二次访问的是服务器2,用户会多次输入密码。
解决上述情况,可以将 session 存在 redis 中。
pom.xml
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
application.yml
spring:
session:
# session 的存储方式
store-type: redis
private CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 设置允许跨域的站点
corsConfiguration.addAllowedOrigin("*");
// 设置允许跨域的http方法
corsConfiguration.addAllowedMethod("*");
// 设置允许跨域的请求头
corsConfiguration.addAllowedHeader("*");
// 允许带凭证
corsConfiguration.setAllowCredentials(true);
// 对所有的url生效
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
// 跨域支持
http.cors().configurationSource(corsConfigurationSource());
// 权限集合
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 设置权限
if (user.getUsername().startsWith("admin")) {
// 给用户名是 admin 开头的用户 设置管理员权限
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
// 设置url 的访问权限 此处是登录成功后能够做的事情
// /demo/** 下的url 需要 admin 权限
// hasRole:指定需要特定的角色的用户允许访问, 会自动在角色前面插入'ROLE_'
http.authorizeRequests().antMatchers("/demo/**").hasRole("ADMIN");
@Service
public class DeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
System.out.println("权限不足,请联系管理员");
}
}
@Autowired
private DeniedHandler deniedHandler;
// 自定义权限不足的信息
http.exceptionHandling().accessDeniedHandler(deniedHandler);
@Service
public class AuthorizationService {
// 自定义权限认证
public boolean checkAuthority(Authentication authentication, HttpServletRequest request) {
User user = (User) authentication.getPrincipal();
Collection<GrantedAuthority> authorities = user.getAuthorities();
for (GrantedAuthority authority : authorities) {
String authorityName = authority.getAuthority();
if ("ROLE_ADMIN".equals(authorityName)) {
System.out.println(user.getUsername() + "有管理员权限");
return true;
}
}
return false;
}
}
// 自定义bean 权限认证http.authorizeRequests().antMatchers("/demo/**").access("@authorizationService.checkAuthority(authentication,request)");
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequestMapping("/findAll")
// 在进入方法前进行验证
@PreAuthorize("hasRole('ADMIN')")
public List<PayingUser> findAll() {
return payingUserDao.findAll();
}
@RequestMapping("/update/{id}")
@PreAuthorize("#id<10")
public String update(@PathVariable Integer id) {
return "针对id小于10的用户可访问";
}
@GetMapping("/get/{id}")
// 可以拿到返回值 此处判断的权限是当前登录用户名和返回的用户名相同
@PostAuthorize("returnObject.username == authentication.principal.username")
public PayingUser get(@PathVariable Integer id) {
return payingUserDao.findById(id).get();
}
@GetMapping("/delByIds")
// 可以对集合类型的参数进行过滤 将不符合条件的元素剔除
// http://127.0.0.1:8080/demo/delByIds?id=1,2,3,4,5,6,7
@PreFilter(filterTarget = "ids", value = "filterObject%2==0")
public void delByIds(@RequestParam(value = "id") List<Integer> ids) {
for (Integer id : ids) {
System.out.println(id); // 2 4 6
}
}
@GetMapping("/findAllPayUser")
// 对集合类型的返回值进行过滤
@PostFilter("filterObject.id % 2==0")
public List<PayingUser> findAllPayUser() {
return payingUserDao.findAll(); // 返回 id = 2,4,6... 的用户
}
需要五张表。
一个用户可以有多个角色。一个角色可以有多个权限。
用户表:
CREATE TABLE `paying_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) COLLATE utf8_bin DEFAULT NULL,
`password` varchar(100) COLLATE utf8_bin DEFAULT NULL,
`status` int(1) DEFAULT NULL COMMENT '用户状态1-启用 0-关闭',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=COMPACT;
角色表:
CREATE TABLE `t_role` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`ROLE_NAME` varchar(30) DEFAULT NULL COMMENT '角色名称',
`ROLE_DESC` varchar(60) DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
权限表:
CREATE TABLE `t_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`permission_name` varchar(30) DEFAULT NULL COMMENT '权限名称',
`permission_tag` varchar(30) DEFAULT NULL COMMENT '权限标签',
`permission_url` varchar(100) DEFAULT NULL COMMENT '权限地址',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
用户-角色关系表:
CREATE TABLE `t_user_role` (
`UID` int(11) NOT NULL COMMENT '用户编号',
`RID` int(11) NOT NULL COMMENT '角色编号',
PRIMARY KEY (`UID`,`RID`) USING BTREE,
KEY `FK_Reference_10` (`RID`) USING BTREE,
CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `t_role` (`ID`),
CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `paying_user` (`id`)
) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
角色-权限关系表:
CREATE TABLE `t_role_permission` (
`RID` int(11) NOT NULL COMMENT '角色编号',
`PID` int(11) NOT NULL COMMENT '权限编号',
PRIMARY KEY (`RID`,`PID`) USING BTREE,
KEY `FK_Reference_12` (`PID`) USING BTREE,
CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `t_role` (`ID`),
CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `t_permission` (`id`)
) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
根据用户 id 查询用户权限的 SQL。
SELECT p.* FROM t_permission p,t_role_permission rp,t_role r,t_user_role ur,paying_user u
WHERE p.id = rp.PID AND rp.RID = r.id AND r.id = ur.RID AND ur.UID = u.id AND u.id = ?
// SecurityConfiguration 添加
// 基于数据库的权限认证
http.authorizeRequests().antMatchers("/demo/**").hasAuthority("user:findAll");
// PayingUserService 添加
List<Permission> permissions = permissionDao.findByUserId(user.getId());
for (Permission permission : permissions) {
// 根据数据库存到内容设置权限
authorities.add(new SimpleGrantedAuthority(permission.getPermissionTag()));
}
参考git:https://gitee.com/zhangyizhou/learning-spring-security-demo.git