安全控制Spring Security

安全控制Spring Security

概念

什么是spring security?

Spring Security是为基于Spring的应用程序提供声明式安全保护的安全性框架。Spring Security提供了完整的安全性解决方案,它能够在web请求级别和方法调用级别处理身份认证和授权。充分利用了依赖注入(DI)和面向切面(AOP)的技术。

安全框架有两个重要的概念:认证(Authentication)和授权(Authorization)。认证即确认用户可以访问当前系统;授权即确定用户在当前系统下所拥有的功能权限。

1.1 简单示例

通过springboot项目添加Spring Security依赖,测试安全控制

        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

通过templates实现一个简单的启动成功提示页面

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>启动成功</title>
</head>
<body>
<div>
    <h1>启动成功</h1>
</div>
</body>
</html>

通过页面接口访问,配置context-path和端口

server.servlet.context-path=/MySpringBootDemo
server.port=8899

启动项目,在启动日志中有密码
在这里插入图片描述
页面访问接口
在这里插入图片描述

用户名:user
密码:启动日志中的
登录成功以后就会进入到提示页面

当然实际项目中不会这样使用的,最简单的方式在配置文件中配置用户名密码,如下:

spring.security.user.name=user
spring.security.user.password=123

即使如此实际项目一般不会这么用,因为直接这样使用spring security会拦截所有访问请求,必须登录才能调用。

  1. 项目中可能某些接口或者页面是可以直接访问的,不需要拦截,那么就需要放行,需要定制拦截;
  2. 项目中有些页面是通过权限控制的,可能只有管理员登录才能访问,普通用户访问不了,那么就要做权限管理,需要制定角色等;
  3. 项目一般会有自己定制化的登录页面,默认的登录页面太简单了。

当然安全控制的场景很多,项目会有不同的定制操作,不会直接依赖默认的Spring Security

1.2 Spring Security的模块

Spring Security3.2分为11个模块
ACL: 支持通过访问控制列表(access control list ACL)为域提供安全性;

切面(Aspect): 一个很小的模块,当时用Spring Security注解时,会使用基于AspectJ的切面,而不是使用标准的Spring AOP;

CAS客户端: 提供与Jasig的中心认证服务(Central Authentication Service CAS)进行集成的功能;

配置(Configuration): 包含通过XML和Java配置Spring Security的功能支持;

核心(core): 提供Spring Security基本库;

加密(Cryptography): 提供了加密和密码编码功能;

LDAP: 支持基于LDAP进行认证;

OpenID: 支持使用OpenID进行集中式认证;

Remoting: 提供了对Spring Remoting的支持;

标签库(Tag Library): Spring Security的JSP标签库;

Web: 提供了Spring Security基于Filter的Web安全性支持;

1.3 Spring Security的配置

开启Spring Security配置很简单,自定义一个配置类,此配置类继承WebSecurityConfigurerAdapter类,通过重写configure方法来配置相关的安全配置,通过注解@EnableWebSecurity开启安全性功能,若应用程序使用SpringMVC,那么可以使用@EnableWebMvcSecurity,次注解配置了一个Spring MVC参数解析器,这样的话处理器方法能够通过带有@AuthenticationPrincipal注解的参数获得认证用户的principal(或username)。例如:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //通过重载,配置user-detail服务
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    //通过重载,配置如何通过拦截器保护请求
    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    //通过重载,配置Spring Security的Filter链
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}

1.4 用户认证

认证需要我们有一套用户数据的来源,而授权则是对于某个用户有相应的角色权限。在Spring Security里我们通过重写以下方法实现定制化。

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

内存用户

AUthenticationManagerBuilder有多个方法可以用来配置Spring Security对认证的支持。通过inMemoryAuthentication()方法,我们可以启用,配置并任意填充基于内存的用户存储。

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user").password("123").roles("ROLE_USER")
                .and()
                .withUser("admin").password("456").authorities("USER","ADMIN");
    }
}

上述示例SecurityConfig重写了configure()方法,填充了两个用户,通过查看源码roles()调用了一下方法,会判断角色的命名,最后返回一个UserBuilder,而UserBuilder是org.springframework.security.core.userdetails.User的内部类,User实现了UserDetails

		public UserBuilder roles(String... roles) {
			List<GrantedAuthority> authorities = new ArrayList<>(
					roles.length);
			for (String role : roles) {
				Assert.isTrue(!role.startsWith("ROLE_"), () -> role
						+ " cannot start with ROLE_ (it is automatically added)");
				authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
			}
			return authorities(authorities);
		}

同样authorites可以查看源码

		public UserBuilder authorities(String... authorities) {
			return authorities(AuthorityUtils.createAuthorityList(authorities));
		}

配置方法如下:
accountExpired(boolean): 定义账号是否已经过期

accountLocked(boolean): 定义账号是否已经锁定

and(): 用来连接配置

authorites(GrantedAuthority…): 授予某个用户一项或者多项权限

authorites(List<? extends GrantedAuthority>): 授予某个用户一项或者多权限

credentialsExpired(boolean): 定义凭证是否已经过期

disabled(boolean): 定义账号是否已经被禁用

password(String): 定义用户密码

roles(String…): 授予某个用户一项或多项角色

数据库用户

用户信息并非总是存在内存中,一般情况下用户信息存在数据库中,那么怎么给数据库中的用户认证授权呢?
可以通过jdbcAuthentication()方法

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource);
    }

默认的Spring Security内部实现了SQL查询语句,类org.springframework.security.provisioning.JdbcUserDetailsManager中定义了SQL查询语句,如下:

    public static final String DEF_USER_EXISTS_SQL = "select username from users where username = ?";

	public static final String DEF_GROUP_AUTHORITIES_QUERY_SQL = "select g.id, g.group_name, ga.authority "
			+ "from groups g, group_authorities ga "
			+ "where g.group_name = ? "
			+ "and g.id = ga.group_id ";

但是我们项目中的数据库用户表不一定是按照默认SQL设计的,那么就需要用到定制化的SQL语句,如下:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery("select user, password, true from t_user where username = ?")
                .authoritiesByUsernameQuery("select username, 'ROLE_USER' from t_user where username = ?");
    }

自定义sql语句,重写认证和基本权限查询语句,所有查询都将用户名作为唯一参数。

一般我们用户的密码是需要加密处理的,那么需要借助passwordEncoder()方法指定一个密码转码器。例如:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery("select user, password, true from t_user where username = ?")
                .authoritiesByUsernameQuery("select username, 'ROLE_USER' from t_user where username = ?")
                .passwordEncoder(new PasswordEncoder() {
                    @Override
                    public String encode(CharSequence rawPassword) {
                        return DigestUtils.md5Hex(rawPassword.toString());
                    }

                    @Override
                    public boolean matches(CharSequence rawPassword, String encodedPassword) {
                        return StringUtils.equals(DigestUtils.md2Hex(rawPassword.toString()),encodedPassword);
                    }
                });
    }

以上通过内部类方式实现了对密码的MD5加密。

通用用户

通过自定义实现UserDetailsService接口,指定用户名,密码以及角色,例如:

@Service
public class CustomUserServiceImpl implements UserDetailsService{

    @Resource
    private UserService userService;

    @Resource
    private UserMapper userMapper;

    @Resource
    private UserRoleMapper userRoleMapper;

    @Resource
    private RoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //获取一个用户
        UserBean user =userMapper.findUserByUserName(username);
        List<GrantedAuthority> authorities = new ArrayList<>();
        
        //设置角色为ROLE_ADMIN
        authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN");

        return new User(user.getUsername(), user.getPassword(), authorities);
    }
}

通过表单提交获取username,比如通过template自定义个一登录页面,提交通过表达提交登录信息,此方法就可以获取到登录名,通过登录名称查询用户信息,当然如果自定义登录页面通过手机号码登录,那么此username即为手机号码。自定义登录也后面会有示例。

同时还需要在配置类中注册这个自定义类

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    UserDetailsService customUserService() {
        return new CustomUserServiceImpl();
    }
    
        @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserService());
    }
    
    .......
}

自定义登录页面表达例如:

					<form th:action="@{/login}" method="post" class="am-form" >
                        <div  class="loginInput am-input-group am-u-lg-10 col-md-offset-2 am-form-icon am-form-feedback" style="margin-left: 0">
                            <div class="am-input-group-label"><i class="fa fa-phone fa-lg"></i></div>
                            <input type="text" name="username" class="am-form-field" id="phone" placeholder="请输入手机号"
                                   onblur="login.checkPhone()" onfocus="login.focusPhone()">
                            <span id="phone_null" class="icons am-icon-warning" style="color: orange"></span>
                            <span id="phone_error" class="icons am-icon-times" style="color: red"></span>
                        </div>
                        <br>
                        <div class="loginInput am-input-group am-u-lg-10 col-md-offset-2 am-form-icon am-form-feedback" style="margin-left: 0">
                            <div class="am-input-group-label"><i class="fa fa-lock fa-lg"></i></div>
                            <input type="password" name="password" id="password" placeholder="请输入密码"
                                   onfocus="login.focusPassword()">
                            <span id="password_null" class="icons am-icon-warning" style="color: orange"></span>
                        </div>
                        <br/>
                        <div class="am-u-lg-10 col-md-offset-2">
                            <label class="rememberMe" for="remember-me">
                                <input id="remember-me" type="checkbox">
                                记住密码
                            </label>
                        </div>
                        <br>
                        <div class="am-input-group am-u-lg-10 col-md-offset-2" style="margin-left: 0">
                            <input type="submit" id="loginFormSubmit" value="登 录" class="am-btn am-btn-primary am-btn-sm am-fl"
                                   style="border-radius: 5px" onclick="login.submit()">
                            <button type="button" class="am-btn am-btn-default am-btn-sm am-fr " data-am-modal="{target: '#my-alert'}" style="border-radius: 5px">
                                忘记密码 ^_^?
                            </button>
                        </div>
                        <div class="login_error" th:if="${param.error}">
                            <span class="am-alert am-alert-secondary" data-am-alert style="margin-left: 15%;background-color: #d85a37;color: white;border-radius: 5px">用户名不存在或密码错误!</span>
                        </div>
                    </form>

1.5 拦截请求

项目中会有很多接口,那么哪些接口哪些接口不需要认证即可访问,哪些需要通过授权认证才能访问,那么安全控制的方式实现拦截,Spring Security可以通过重写以下方法实现:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }

Spring Security使用以下匹配器来匹配请求路径:

  • antMatchers: 使用Ant风格的路径匹配
  • regexMatchers: 使用正则表达式匹配路径
  • anyRequest: 匹配所有请求路径

安全处理方法:

方法用途
access(string)Spring EL表达式结果为true时可以访问
anonymous()匿名可访问
permitAll()用户可任意访问
authenticated登录以后可访问
denyAll用户不能访问
hasAnyAuthority(String…)如果用户有参数,则其中任一权限可访问
hasAnyRole(String…)如果用户有参数,则其中任一角色可访问
hasAuthority(String)如果用户有参数,则其权限可访问
hasIpAddress(String)如果用户来自参数中的IP则可以访问
hasRole(String)若用户有参数中的角色可访问
rememberMe()允许通过remember-me登录访问
fullyAuthenticated()用户完全认证可访问(非remember me下自动登录)

具体使用方式可以根据源码,例如:hasAnyRole(String…)
通过查看源码发现角色都是“ROLE_”开头

	private static String hasAnyRole(String... authorities) {
		String anyAuthorities = StringUtils.arrayToDelimitedString(authorities,
				"','ROLE_");
		return "hasAnyRole('ROLE_" + anyAuthorities + "')";
	}

与hasAnyAuthority(String…)的不同,如下:

	private static String hasAnyAuthority(String... authorities) {
		String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','");
		return "hasAnyAuthority('" + anyAuthorities + "')";
	}

也就是“ROLE_”开头的认为是角色,否则认为是权限。

例如:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/", "/index")
                .permitAll()
                .antMatchers("/editor", "/user").hasAnyRole("USER")
                .antMatchers("/ali", "/mylove").hasAnyAuthority()
                .antMatchers(HttpMethod.POST, "/superadmin").hasAnyRole("SUPERADMIN")
                .and()
                .formLogin().loginPage("/login").failureUrl("/login?error").defaultSuccessUrl("/user")
                .and()
                .headers().frameOptions().sameOrigin()
                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/")
                .and()
                .rememberMe()
                .tokenValiditySeconds(2419200)
                .key("blogKey");
        http.csrf().disable();
    }

antMatchers()有两个重载的方法,分别是:

public C antMatchers(HttpMethod method, String... antPatterns)

public C antMatchers(String... antPatterns)

方法1可以指定请求接口的类型;方法二不需要指定,只通过接口地址拦截。

1.6 定制登录

定制化登录也可以重写以下方法

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }

例如:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                    .loginPage("/login")    //登录页面接口
                    .failureUrl("/login?error")     //登录失败页面接口
                    .defaultSuccessUrl("/")     //默认登录成功以后跳转页面接口地址
                .and()      //连接不同配置指令
                .rememberMe()       //开启cookie保存用户信息
                    .tokenValiditySeconds(1209600)      //token的过期时间
                    .key("myKey")       //cookie的私钥
                .and()
                .logout()   //退出登录
                    .logoutUrl("/logout")   //退出登录接口地址
                    .logoutSuccessUrl("/");     //退出成功跳转页面
        http.csrf().disable();      //禁用CSRF防护功能,可以跨域访问
    }

formLogin() 定制登录操作,查看源码发现调用了FormLoginConfigurer类

	public FormLoginConfigurer() {
		super(new UsernamePasswordAuthenticationFilter(), null);
		usernameParameter("username");
		passwordParameter("password");
	}

获取参数username,password即用户名和密码,再跟进UsernamePasswordAuthenticationFilter

    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }
    
    public AntPathRequestMatcher(String pattern, String httpMethod) {
        this(pattern, httpMethod, true);
    }

loginPage(): 定制登录页面地址
failureUrl() : 定制登录失败地址
rememberMe() : 开启cookie存储用户信息
tokenValiditySeconds(): 指定 cookie有效期
key(): 指定cookie中私钥
logout(): 注销行为
logoutUrl(): 注销的Url地址
logoutSuccessUrl(): 注销成功后转向的页面

1.7 保护视图

SpringBoot中使用template自定义页面的时候,可能有些功能展示需要根据不同角色,Spring Security也提供了一些标签:
sec:authentication —— 渲染认证对象的属性
sec:authorize —— 基于表达式的计算结果,条件性的渲染内容
sec:authorize-acl —— 基于表达式的计算结果,条件性的渲染内容
sec:authorize-expr —— sec:authorize属性的别名
sec:authorize-url —— 基于给定URL路径相关的安全规则,条件性的渲染内容

当然是用这些标签的话与需要在html中添加依赖,例如:

xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"

比如登录成功以后需要显示用户名以及点击按钮,例如:

<button class="personalSpace am-btn am-btn-secondary am-topbar-btn am-btn-sm am-dropdown-toggle"
                                data-am-dropdown-toggle>Hello<span sec:authentication="name"></span> <span
                                class="am-icon-caret-down"></span></button>

未登录
在这里插入图片描述
登录成功
在这里插入图片描述

当然登录成功以后有些菜单可以通过不同角色判断是否显示,例如:

                        <ul class="am-dropdown-content">
                            <li><a href="/user" sec:authorize="hasRole('ROLE_USER')">个人主页</a></li>
                            <li><a href="/superadmin" sec:authorize="hasRole('ROLE_SUPERADMIN')">后台管理</a></li>
                            <li><a href="/user" class="news">消息<span
                                    class="newsNum am-badge am-badge-danger"></span></a></li>
                            <li><a class="feedbackClick">反馈</a></li>
                            <li><a th:href="@{/logout}">退出登录</a></li>
                        </ul>

使用sec:authorize 标签判断不同角色,以上示例中“个人主页”角色为ROLE_USER的可以查看,而后台管理需要ROLE_SUPERADMIN角色才能查看,而反馈是没有限定的。

以上即为Spring Security的一个简单入门学习。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值